Hyperliquid WebSocket API: subscriptions, limits and a Python example
Hyperliquid streams market and account data over one WebSocket endpoint per network. This page covers the URLs, the subscribe message, every subscription type in Hyperliquid's WebSocket docs, snapshots, post requests, heartbeats, reconnects and limits. We checked each message shape against wss://api.hyperliquid.xyz/ws on 27 October 2026.
The short version#
- Mainnet:
wss://api.hyperliquid.xyz/ws. Testnet:wss://api.hyperliquid-testnet.xyz/ws. - Subscribe:
{"method":"subscribe","subscription":{...}}. Data arrives on a channel named after the type. - Types: 24 in the docs. 9 market feeds and 15 per-user feeds.
- Heartbeat: send
{"method":"ping"}. The server closes a connection after 60 seconds without a message. - Limits per IP: 10 connections, 1000 subscriptions, 10 unique users, 2000 messages per minute, 100 inflight posts.
1. Endpoints#
- Mainnet:
wss://api.hyperliquid.xyz/ws - Testnet:
wss://api.hyperliquid-testnet.xyz/ws
Both URLs take subscriptions and post requests. No API key or login is needed to connect. Account feeds take a public address and need no signature. Only exchange actions sent as post requests carry a signature.
2. Try it from a terminal#
websocat sends each line of stdin as one message and prints each message it receives on its own line. -n keeps the socket open after echo ends. Without it websocat closes the connection before any data arrives.
# install: brew install websocat, or cargo install websocat
echo '{"method":"subscribe","subscription":{"type":"trades","coin":"BTC"}}' \
| websocat -n wss://api.hyperliquid.xyz/wsThe first two lines we got back, trimmed to one trade, with hashes and addresses shortened:
{"channel":"subscriptionResponse","data":{"method":"subscribe","subscription":{"type":"trades","coin":"BTC"}}}
{"channel":"trades","data":[{"coin":"BTC","side":"B","px":"79867.0","sz":"0.0015","time":1793089431804,"hash":"0x1dec...929c","tid":908348318513607,"users":["0xc7ff...a487","0x7abe...c4d1"]}]}Pipe it into jq to get one compact line per trade:
# one line per trade: time, side, price, size
echo '{"method":"subscribe","subscription":{"type":"trades","coin":"BTC"}}' \
| websocat -n wss://api.hyperliquid.xyz/ws \
| jq -c 'select(.channel=="trades") | .data[] | [.time, .side, .px, .sz]'Hyperliquid's own docs use wscat -c wss://api.hyperliquid.xyz/ws and a pasted subscribe message. Both tools show the same JSON.
3. Subscribe and unsubscribe#
{ "method": "subscribe", "subscription": { "type": "l2Book", "coin": "ETH" } }The server acks on channel subscriptionResponse and echoes the subscription with defaults filled in. This is the ack we got for the message above:
{"channel":"subscriptionResponse","data":{"method":"subscribe","subscription":{"type":"l2Book","coin":"ETH","nSigFigs":null,"mantissa":null,"fast":false}}}Data then arrives with channel set to the type: l2Book, trades, allMids. One exception: userEvents data arrives on channel user. To stop a feed, send the same subscription object with "method": "unsubscribe". The ack comes back with "method":"unsubscribe".
Errors arrive on channel error as a string. The two we hit while testing:
Already subscribed: {...}when the same subscription is sent twice.Error parsing JSON into valid websocket request: ...for an unknown type or a malformed field.
Coins use the names from the info API: BTC, ETH. Builder-deployed perps carry their dex prefix, for example xyz:NVDA. For user feeds, pass the master account or sub-account address. The docs warn that an agent wallet address returns empty results.
4. Subscription types#
The Subscriptions doc page lists 24 types. A ? marks a field the docs call optional. Market feeds take a coin, a dex or nothing:
| Type | Fields | What it streams |
|---|---|---|
| allMids | dex? | Mid price of every coin on one perp dex. Without dex: the first perp dex, plus spot. |
| l2Book | coin, nSigFigs?, mantissa?, fast? | Book snapshot on each push. 20 levels per side, or 5 with fast: true. |
| trades | coin | Trades with price, size, side, hash, tid and both addresses. |
| candle | coin, interval | The open candle for one interval, 1m to 1M. |
| bbo | coin | Best bid and offer. Sent only when it changes on a block. |
| activeAssetCtx | coin | Mark, mid and oracle price, funding, open interest, day volume. |
| allDexsAssetCtxs | none | Asset contexts for every perp dex. |
| fastAssetCtxs | none | Mark and mid prices, base64 raw DEFLATE. A snapshot, then changed coins only. |
| outcomeMetaUpdates | none | Outcome markets and questions created, updated or settled. |
User feeds take an address. Each one counts toward the cap of 10 unique users per IP:
| Type | Fields | What it streams |
|---|---|---|
| notification | user | Notification strings for the user. |
| webData3 | user | Aggregate user state, used by the Hyperliquid frontend. |
| clearinghouseState | user, dex | Positions and margin summary on one perp dex. |
| allDexsClearinghouseState | user | Positions and margin on every perp dex. |
| openOrders | user, dex | Open orders. |
| orderUpdates | user | Order status changes. |
| userEvents | user | Fills, funding, liquidations and non-user cancels. Arrives on channel user. |
| userFills | user, aggregateByTime? | Fills. A snapshot, then each new fill. |
| userFundings | user | Funding payments. A snapshot, then each hourly payment. |
| userNonFundingLedgerUpdates | user | Deposits, withdrawals, transfers, liquidations and vault events. |
| activeAssetData | user, coin | Leverage, max trade sizes and available to trade. Perps only. |
| spotState | user, isPortfolioMargin? | Spot balances. |
| twapStates | user, dex | Running TWAP orders. |
| userTwapSliceFills | user | Fills from TWAP slices. |
| userTwapHistory | user | TWAP status changes: activated, finished, terminated, error. |
Candle intervals are 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 8h, 12h, 1d, 3d, 1w and 1M.
Feed rates we measured#
Mainnet, 27 October 2026, median gap between messages over 20 to 30 seconds:
bboon BTC: 0.12 s.l2Bookwithfast: true: 0.53 s, 5 levels per side.l2Bookdefault: 5.3 s, 20 levels per side.activeAssetCtx: 1 s.allMids: 5 s.
Use bbo for top of book and l2Book with fast: true for depth that keeps up. Prices and sizes are JSON strings. In our session the candle o, c, h, l, v fields and the activeAssetCtx fields were strings too, although the docs type them as numbers. Parse them with a decimal type.
5. Snapshots and isSnapshot#
Feeds that carry history send earlier data first. The first userFills, userFundings, userTwapSliceFills and userTwapHistory message has isSnapshot: true. Every later message has isSnapshot: false. The first userFills message we got, trimmed:
{"channel":"userFills","data":{"isSnapshot":true,"user":"0x...","fills":[
{"coin":"xyz:CL","px":"102.23","sz":"10.095","side":"B","time":1793089293710,"dir":"Open Long",
"closedPnl":"0.0","oid":587071587694,"crossed":false,"fee":"0.01486","tid":788163204116105,"feeToken":"USDC"}
]}}- Each
userFillssnapshot we received held 30 fills. We subscribed to 15 active wallets. - The first
tradesmessage after the ack held the 30 most recent trades. It has noisSnapshotfield. - For older fills use the
userFillsByTimeinfo request, or the S3 archive in the historical data guide.
The docs say snapshot messages can be ignored if you already processed them. After a reconnect, drop snapshot fills you already stored. The docs call a fill's tid a unique trade id. For public trades they give (time, coin, tid) as the unique key.
6. Post requests#
Any info request or signed exchange action can go over the socket instead of HTTP. Wrap it in a post message:
{
"method": "post",
"id": 1,
"request": {
"type": "info",
"payload": { "type": "l2Book", "coin": "ETH", "nSigFigs": 5 }
}
}The reply arrives on channel post with the same id:
{"channel":"post","data":{"id":1,"response":{"type":"info","payload":{"type":"l2Book","data":{"coin":"ETH","time":1793089326682,"levels":[[...],[...]]}}}}}methodandidare mandatory. Use a unique id per request to match replies.- For orders and cancels, set
"type": "action"and send the signed payload the/exchangeendpoint takes: action, nonce, signature, vault address. - An error comes back as
"type": "error"with a string that mirrors the HTTP status. - Explorer requests are not supported over WebSocket.
- At most 100 post messages can be in flight at once, across all connections.
7. Heartbeats#
The server closes a connection after 60 seconds without a message. A feed for a quiet account can go silent for longer than that. Send a ping:
{ "method": "ping" }The server answers {"channel":"pong"}. In our test an idle connection closed at 61.5 seconds with code 1000 and reason Inactive. A ping at 45 seconds kept a second connection open. Send one every 50 seconds on a quiet connection.
8. Reconnect and resubscribe#
The docs say disconnects happen periodically and without notice, and every automated client should reconnect. Subscriptions do not carry over to a new connection.
- Keep the list of subscription objects in your code.
- On close, reconnect with a growing delay. Start at 1 second and cap near 30. The limit is 30 new connections per minute per IP.
- Send every subscribe message again.
- Merge the snapshot. The docs say data missed during the reconnect is in the snapshot ack.
- For anything still missing, send the matching info request, over HTTP or as a post.
9. WebSocket limits#
From Hyperliquid's rate limit docs. All of them apply per IP:
- 10 connections, and 30 new connections per minute.
- 1000 subscriptions.
- 10 unique users across user-specific subscriptions.
- 2000 messages sent to Hyperliquid per minute, across all connections.
- 100 simultaneous inflight post messages, across all connections.
Past the user cap, the subscribe fails on channel error. In our test the server accepted 15 users on one connection and answered the 16th with Cannot track more than 15 total users. Plan for the documented 10. The REST limits are in the rate limits guide.
10. Python example#
Subscribes to BTC trades and the ETH book, sends a ping every 50 seconds, and reconnects and resubscribes after a disconnect. We ran it against mainnet with websockets 17.3.
# pip install websockets
import asyncio
import json
import websockets
URL = "wss://api.hyperliquid.xyz/ws"
SUBS = [
{"type": "trades", "coin": "BTC"},
{"type": "l2Book", "coin": "ETH"},
]
async def keepalive(ws):
# Hyperliquid closes a connection that sends nothing for 60 seconds.
while True:
await asyncio.sleep(50)
await ws.send(json.dumps({"method": "ping"}))
def handle(msg):
channel = msg["channel"]
if channel == "trades":
for t in msg["data"]:
print("trade", t["coin"], t["side"], t["px"], t["sz"])
elif channel == "l2Book":
bids, asks = msg["data"]["levels"]
print("book", msg["data"]["coin"], bids[0]["px"], asks[0]["px"])
elif channel == "error":
print("error", msg["data"])
async def main():
# connect() as an iterator reconnects with exponential backoff.
async for ws in websockets.connect(URL):
pinger = asyncio.create_task(keepalive(ws))
try:
for sub in SUBS:
await ws.send(json.dumps({"method": "subscribe", "subscription": sub}))
async for raw in ws:
handle(json.loads(raw))
except websockets.ConnectionClosed:
continue
finally:
pinger.cancel()
asyncio.run(main())It prints lines such as trade BTC B 79767.0 0.0734 and book ETH 2312.7 2312.8, the best bid and ask.
The official hyperliquid-python-sdk has a WebSocket manager that also sends a ping every 50 seconds, if you already use the SDK.
11. When WebSockets are not enough#
The cap of 10 unique users per IP stops wallet tracking early. A watch list of 500 wallets does not fit. Two sources have no user cap.
A node's fill files#
A non-validator node started with --write-fills writes every fill for every address to hourly files under ~/hl/data/node_fills_by_block/. Read the newest file as it grows and keep the addresses you track. Reading files on your own disk does not touch the public API or its limits. Setup is in the node guide.
Fills over REST#
Ironflow indexes the same fills from its own nodes. One request returns the fills of any wallet:
curl "https://api.ironflow.sh/v1/fills?address=0xYOUR_ADDRESS&limit=100"Without a key you get 10 requests per minute and 24 hours of history. A free key gives 60 per minute and 30 days. The docs list every endpoint.
Questions#
What is the Hyperliquid WebSocket URL?#
wss://api.hyperliquid.xyz/ws on mainnet and wss://api.hyperliquid-testnet.xyz/ws on testnet. Both take subscriptions and post requests.
How do I subscribe to the Hyperliquid WebSocket?#
Send {"method":"subscribe","subscription":{"type":"trades","coin":"BTC"}}. The server acks on channel subscriptionResponse, then streams on a channel named after the type, here trades.
How many wallets can I track over the Hyperliquid WebSocket?#
The docs allow 10 unique users across user-specific subscriptions per IP. Every subscription with a user field counts. To follow more wallets, read fills from a node or from an indexed API.
Why does my Hyperliquid WebSocket disconnect after 60 seconds?#
The server closes a connection after 60 seconds without a message, with close reason Inactive. Send {"method":"ping"} every 50 seconds or so. The server answers {"channel":"pong"}.
Can I place orders over the Hyperliquid WebSocket?#
Yes. Send a post request with type action and the same signed payload the /exchange endpoint takes. The response arrives on channel post with your id.
What does isSnapshot mean in Hyperliquid WebSocket messages?#
Feeds such as userFills and userFundings first send earlier data with isSnapshot: true. Later messages have isSnapshot: false. Skip snapshot items you already stored.
Related guides#
- Hyperliquid API rate limits explained
- How to run a Hyperliquid non-validator node
- Hyperliquid historical data: API, S3 archive and Python
References#
- Hyperliquid docs: WebSocket. Endpoints and reconnect guidance.
- Hyperliquid docs: Subscriptions. The source for every subscription type and data format on this page.
- Hyperliquid docs: Post requests. Info requests and signed actions over WebSocket.
- Hyperliquid docs: Timeouts and heartbeats. The 60 second timeout and the ping message.
- Hyperliquid docs: Rate limits and user limits. Connection, subscription, user and message limits.
- hyperliquid-python-sdk websocket_manager.py. Official SDK connection code.
- websocat. Command-line WebSocket client used in section 2.
We run read-only Hyperliquid nodes that write every fill for every wallet, with no WebSocket user cap. Details are on the nodes page. Fills for any wallet over REST are on the API page.