Session Management and Reconnection
Session persistence, reconnection, and automatic replay for reliable WebSocket connections
Sessions enable reliable WebSocket connections with automatic replay of missed data on reconnect.
Overview
When you connect to the WebSocket API, you receive a sessionId that persists your subscriptions and tracks your position in the data stream. On reconnect, provide this session ID to:
Restore subscriptions - No need to re-subscribe
Receive replay - Automatically get missed data (up to 30 seconds)
Resume seamlessly - Continue from where you left off
Key Concepts
Sequence Numbers (seq)
seq)Every live data message includes a seq field - a monotonically increasing integer for client use only:
{
"type": "allFills",
"seq": 42,
"fills": [...]
}Purpose: Detect gaps within a single connection. If you receive seq: 5 then seq: 7, you know seq: 6 was lost.
Key behaviors:
Resets to 1 on every new connection (including reconnects)
Per-subscription channel (each subscription has its own sequence)
Client-side only - server doesn't track your last received seq
Cursor
The cursor tracks your position in the data stream, tied to Hyperliquid's block production. The server supports two cursor formats:
Cursor Format
Cursors use a colon-separated format with up to 3 components:
block:timestamp:tx_index
"500:1706123456789:3"
Full precision — most subscription types
block:timestamp
"500:1706123456789"
Block-level — used by l4BookUpdates, fundingRates, and setOracleUpdates
timestamp (number)
1706123456789
Numeric — used by leverage and isolated margin update subscriptions
The tx_index component enables precise sub-block positioning. Within a single block (~70ms), there can be hundreds of events. Without tx_index, resuming from a cursor could miss events or produce duplicates when boundaries fall mid-block.
Treat cursors as opaque strings — always store and return the full cursor exactly as received. The server handles all parsing.
Cursor format: The server always sends cursors in block:timestamp:tx_index format (or block:timestamp for block-level subscriptions). Clients should treat cursors as opaque strings.
Client-side tracking (recommended):
Every live event message includes a
cursorfieldSave this cursor as a string after successfully processing each message
Provide your cursor on reconnect for accurate replay
Server-side tracking (fallback):
The server also tracks a cursor for your session
Persists across disconnects
Used as fallback if you don't provide your own cursor
Important: The server cursor is based on messages sent, not messages received. Due to network issues, OS socket buffering, or client-side delays, you may not have actually received all messages the server sent. For best accuracy, always track and provide your own cursor based on messages you successfully processed.
Manual cursor provision: When subscribing, you can optionally provide a cursor to request replay from a specific point:
This is useful for:
Resuming from a known checkpoint after application restart
Requesting specific historical data within the cache window (30 seconds)
Message Types
Control Messages
connected
Server→Client
New session created
reconnected
Server→Client
Existing session resumed
subscriptionUpdate
Server→Client
Subscription confirmed
ping
Server→Client
Heartbeat (respond with pong)
pong
Client→Server
Heartbeat response
error
Server→Client
Error occurred
Data Messages (Live Events)
Live event messages have the subscription type as their type field:
type
string
Subscription type (e.g., "allFills")
seq
number
Sequence number for gap detection (resets on reconnect)
cursor
string
Position cursor (opaque string, e.g. "500:1706123456789:3")
fills/updates/events/trades/liquidations/data
array
Event data (field name varies by subscription type)
Common data message types: allFills, userFills, userOrderUpdates, liquidationFills, l4BookUpdates, etc.
Replay Messages
Replay messages are distinct from live events - they have "type": "replay" and contain historical data:
type
string
Always "replay"
channel
string
The subscription type (e.g., "allFills")
cursor
string
Per-chunk cursor derived from the last event in this chunk (same format as live cursor). Use this for reconnection.
replayTimeMs
string
Same as cursor. Deprecated — use cursor instead.
count
number
Number of items in this chunk
chunk
number
Current chunk number (1-indexed)
totalChunks
number
Total chunks in this replay
hasGap
boolean
Only present when true. Indicates cache didn't cover entire disconnect period (only meaningful on first chunk). Absent when there is no gap.
data
array
Array of missed events (same format as live data)
Chunking
Large replays are split into multiple chunks to keep message sizes manageable. Chunking occurs when either limit is reached:
Max items: 2,000 items per chunk
Max size: 1 MB per chunk
Use chunk and totalChunks to track replay progress:
Update your cursor from each chunk's
cursoras it arrives — this ensures progress is saved even if the connection drops mid-replayhasGapis only set on the first chunk (chunk: 1)Process chunks in order as they arrive
Important: Replay messages do NOT have a seq field. Only live event messages have sequences.
Connection Flow
Initial Connection
Server responds with connected:
Save the sessionId - you'll need it for reconnection.
Subscribing
Server confirms with subscriptionUpdate:
Then live data begins flowing with seq starting at 1.
Reconnection Flow
Step-by-Step Process
Client disconnects (network issue, restart, etc.)
Server marks session disconnected - starts 30-second grace period
Client reconnects with sessionId and cursor:
Server validates session - checks if still within grace period
Server automatically restores subscriptions - no need to re-subscribe
Server sends
reconnected:Server sends replay - events since your cursor (up to 30 seconds of data)
Live events resume - with
seqstarting at 1
If your original connection used liveFormat=chunked-v1, you must include liveFormat=chunked-v1 again on reconnect. liveFormat is connection-level and is not persisted on the session.
Providing Your Cursor on Reconnect
When reconnecting, you can provide your last successfully processed cursor as a query parameter:
If you are using live chunking, include liveFormat=chunked-v1 as well:
token
Yes
Your API key
sessionId
No
Session ID to resume (omit for new session)
cursor
No
Last cursor you received (opaque string, e.g. "500:1706123456789:3")
liveFormat
No
Required on every reconnect if you are using chunked-v1 live message format
Cursor priority:
Client-provided cursor (recommended) - from the
cursorquery parameterServer-stored cursor (fallback) - used if you don't provide one
Best Practice: Always track the cursor field from each message you process successfully, and provide it on reconnect. This ensures you receive exactly the data you missed, even if some messages were lost in transit before disconnect.
Automatic Subscription Restore: You do not need to re-subscribe after reconnecting. The server automatically restores all your previous subscriptions and begins sending data immediately. The subscriptions array in the reconnected message confirms which feeds were restored.
Note: lastSeq is always 0 because sequences reset on each connection.
What Triggers Replay
Replay is sent when ALL of these conditions are met:
You reconnect with a valid, non-expired sessionId
The session had active subscriptions before disconnect
There is cached data newer than your cursor
The cache TTL (30 seconds) hasn't fully expired
When Replay is NOT Sent
New connection (no sessionId provided)
Expired session (disconnected > 30 seconds ago)
No cached data for your subscriptions (quiet market)
Your cursor is already current (instant reconnect)
Gap Detection
The hasGap field in replay messages indicates data completeness:
hasGap
Meaning
Action
absent
Complete replay - no data lost
Process normally
true
Some data may be missing
Consider fetching from REST API
A gap occurs when your cursor is older than the oldest cached event - some events aged out before you reconnected.
Replay Response Scenarios
When you reconnect with a session, you'll receive one of these replay response types depending on market activity and your disconnect duration:
Scenario
count
hasGap
data
Meaning
Data, no gap
> 0
absent
Events array
Normal replay - you have all missed events
Data, with gap
> 0
true
Events array
Partial replay - some older events expired from cache
No data, with gap
0
true
[]
Cache empty and cursor is old - all events expired
No data, no gap
0
absent
[]
Quiet market - no events occurred during disconnect
Example Responses
Data with no gap (complete replay):
Note: hasGap is absent here because there is no gap. The field is only included when true.
Data with gap (partial replay - some events expired):
No data with gap (all cached events expired):
No data, no gap (quiet market - nothing happened):
Quiet Markets: A replay with count: 0 and hasGap: false confirms you're fully up-to-date - there simply were no events during your disconnect period. This is common for user-specific subscriptions (e.g., userFills) where activity is infrequent.
Handling Gaps: When hasGap: true, consider fetching historical data from the REST API to fill in missing events. The replay still contains all available cached data - it just doesn't cover your entire disconnect period.
Message Ordering
Important: Live messages may begin arriving before replay is complete. Your client must handle this correctly.
Why This Happens
For minimal latency, the server:
Adds you to live broadcast immediately on reconnect
Sends replay data in parallel
In high-throughput scenarios, a live event may arrive before the final replay chunk.
Recommended Client Pattern
Deduplication
Important: Replay and live events may overlap. The same event can appear in both the replay data and the first live messages. Your client must deduplicate events to avoid processing them twice.
Why Overlap Occurs
To ensure no events are lost, the server:
Subscribes you to live broadcasts immediately on reconnect
Generates and sends replay data in parallel
This means events occurring during the brief window between these steps may appear in both streams. This is intentional - it's better to receive a duplicate than to miss an event.
Deduplication Keys
Each event type has a monotonically increasing key composed of (time, txIndex) (or similar). Since these are ordered, you only need to track the last seen key and skip any events at or before it.
allFills, userFills, builderFills, liquidationFills
(time, txIndex)
fill.time, fill.txIndex
userOrderUpdates, builderOrderUpdates
(time, txIndex)
update.time, update.txIndex
allTwapStatusUpdates
(time, txIndex)
update.time, update.txIndex
builderLiquidations, allBuilderLiquidations
(time, txIndex)
fill.time, fill.txIndex
allCompletedTrades, userCompletedTrades, builderCompletedTrades
(closeTime, txIndex)
trade.closeTime, trade.txIndex
userNonFundingLedgerEvents, allUserNonFundingLedgerEvents
(time, txIndex, role)
event.time, event.txIndex, user role
Deduplication Example (Fills)
Since keys are monotonically increasing, track the last processed (time, txIndex) and skip events at or before it:
No Memory Cleanup Needed: Since you only track the last seen key (not a set of all seen IDs), there's no memory growth over time.
Session Lifetime
Grace period
30 seconds
Time to reconnect before session expires
Redis TTL
60 seconds
Total time session persists in Redis (grace + buffer)
Replay cache
30 seconds
Maximum lookback for replay data
Cross-instance
Supported
Sessions persist across server restarts
Timeline Example
Examples
Best Practices
Always save the sessionId from
connectedmessagesAlways pass sessionId when reconnecting
Track the cursor from each successfully processed message
Always pass your cursor when reconnecting for accurate replay
Distinguish replay from live - check
msg.type === 'replay'Buffer live messages during replay processing
Deduplicate events - replay and live may overlap (see Deduplication)
Handle
hasGap: trueby fetching missing data from REST API if criticalTrack sequences within a connection to detect gaps
Implement exponential backoff for reconnection attempts
Respond to pings within 150 seconds to keep connection alive
Error Handling
Session Expired
If your session expired, you'll receive connected instead of reconnected:
This means:
A new session was created
You must re-subscribe to your feeds
No replay will be sent
Detection: Check msg.type === 'reconnected' to confirm successful resume.
Common Errors
"Session not found"
Invalid or expired sessionId
Create new session, re-subscribe
"Invalid API key"
Bad token
Check API key
"Rate limit exceeded"
Too many connections/messages
Implement backoff
"Connection timeout"
Missed ping/pong
Respond to pings within 150s
Summary
sessionId
Persist subscriptions across reconnects
Save and reuse
seq
Detect gaps within a connection
Track and validate
cursor
Stream position (block:ts:tx_index)
Save from each message and replay chunk, provide on reconnect
Replay
Historical data on reconnect
Buffer live until complete
Deduplication
Replay/live events may overlap
Track (time, txIndex), skip events at or before it
hasGap
Only present when true - incomplete replay
Fetch from REST if critical
Auto-restore
Subscriptions restored automatically
None - just reconnect with sessionId
Remember: The server's cursor tracks what was sent, not what you received. For guaranteed data continuity, always track and provide your own cursor based on messages you successfully processed.
Last updated