IRONFLOW
PRODUCTMCPPRICINGNODESDOCSOPEN_TERMINAL
OPEN_TERMINAL
GUIDES · UPDATED 27 OCTOBER 2026 · 8 MIN READ

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.

CONTENTS
  1. The short version
  2. 1. Endpoints
  3. 2. Try it from a terminal
  4. 3. Subscribe and unsubscribe
  5. 4. Subscription types
  6. 5. Snapshots and isSnapshot
  7. 6. Post requests
  8. 7. Heartbeats
  9. 8. Reconnect and resubscribe
  10. 9. WebSocket limits
  11. 10. Python example
  12. 11. When WebSockets are not enough
  13. Questions
  14. Related guides
  15. References

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.

bash
# install: brew install websocat, or cargo install websocat
echo '{"method":"subscribe","subscription":{"type":"trades","coin":"BTC"}}' \
  | websocat -n wss://api.hyperliquid.xyz/ws

The first two lines we got back, trimmed to one trade, with hashes and addresses shortened:

json
{"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:

bash
# 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#

json
{ "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:

json
{"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:

TypeFieldsWhat it streams
allMidsdex?Mid price of every coin on one perp dex. Without dex: the first perp dex, plus spot.
l2Bookcoin, nSigFigs?, mantissa?, fast?Book snapshot on each push. 20 levels per side, or 5 with fast: true.
tradescoinTrades with price, size, side, hash, tid and both addresses.
candlecoin, intervalThe open candle for one interval, 1m to 1M.
bbocoinBest bid and offer. Sent only when it changes on a block.
activeAssetCtxcoinMark, mid and oracle price, funding, open interest, day volume.
allDexsAssetCtxsnoneAsset contexts for every perp dex.
fastAssetCtxsnoneMark and mid prices, base64 raw DEFLATE. A snapshot, then changed coins only.
outcomeMetaUpdatesnoneOutcome markets and questions created, updated or settled.

User feeds take an address. Each one counts toward the cap of 10 unique users per IP:

TypeFieldsWhat it streams
notificationuserNotification strings for the user.
webData3userAggregate user state, used by the Hyperliquid frontend.
clearinghouseStateuser, dexPositions and margin summary on one perp dex.
allDexsClearinghouseStateuserPositions and margin on every perp dex.
openOrdersuser, dexOpen orders.
orderUpdatesuserOrder status changes.
userEventsuserFills, funding, liquidations and non-user cancels. Arrives on channel user.
userFillsuser, aggregateByTime?Fills. A snapshot, then each new fill.
userFundingsuserFunding payments. A snapshot, then each hourly payment.
userNonFundingLedgerUpdatesuserDeposits, withdrawals, transfers, liquidations and vault events.
activeAssetDatauser, coinLeverage, max trade sizes and available to trade. Perps only.
spotStateuser, isPortfolioMargin?Spot balances.
twapStatesuser, dexRunning TWAP orders.
userTwapSliceFillsuserFills from TWAP slices.
userTwapHistoryuserTWAP 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:

  • bbo on BTC: 0.12 s.
  • l2Book with fast: true: 0.53 s, 5 levels per side.
  • l2Book default: 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:

json
{"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 userFills snapshot we received held 30 fills. We subscribed to 15 active wallets.
  • The first trades message after the ack held the 30 most recent trades. It has no isSnapshot field.
  • For older fills use the userFillsByTime info 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:

json
{
  "method": "post",
  "id": 1,
  "request": {
    "type": "info",
    "payload": { "type": "l2Book", "coin": "ETH", "nSigFigs": 5 }
  }
}

The reply arrives on channel post with the same id:

json
{"channel":"post","data":{"id":1,"response":{"type":"info","payload":{"type":"l2Book","data":{"coin":"ETH","time":1793089326682,"levels":[[...],[...]]}}}}}
  • method and id are mandatory. Use a unique id per request to match replies.
  • For orders and cancels, set "type": "action" and send the signed payload the /exchange endpoint 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:

json
{ "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.

python
# 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:

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

ON THIS PAGE
  1. The short version
  2. 1. Endpoints
  3. 2. Try it from a terminal
  4. 3. Subscribe and unsubscribe
  5. 4. Subscription types
  6. 5. Snapshots and isSnapshot
  7. 6. Post requests
  8. 7. Heartbeats
  9. 8. Reconnect and resubscribe
  10. 9. WebSocket limits
  11. 10. Python example
  12. 11. When WebSockets are not enough
  13. Questions
  14. Related guides
  15. References

Automated derivatives trading is high-risk: you can lose all of your funds. Ironflow is trading software, not financial advice, and never holds your funds. Not available to U.S. persons or in sanctioned jurisdictions. See Risk Disclosure.

Not affiliated with Hyperliquid or Pear Protocol.

IRONFLOW

© 2026 IRONFLOW

APIPricingDocsNodesGuidesFree API keyGitHubTermsPrivacyRiskAcknowledgements