Session Management and Reconnection
Session persistence, reconnection, and automatic replay for reliable WebSocket connections
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)
Every live data message includes a seq field - a monotonically increasing integer for client use only:
{
"type": "allFills",
"channel": "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 is your position in the data stream. It is an opaque string: store the latest one you received and send it back unchanged when you reconnect. The server handles all parsing.
Every live message and every replay chunk carries a cursor naming the last event in that message, precise to the individual event, so resuming from it neither misses nor duplicates events. Live-only channels with no replay (bbo, l2Book, allActiveAssetCtx, setOracleUpdates) send the sentinel "0".
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 client subscription type in both type and channel. The values are always equal:
type
string
Subscription type (e.g., "allFills")
channel
string
Same value as type; either field can be used for routing
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
hasMore
boolean
Always present. true while more chunks follow; false on the terminal chunk. Prefer it over chunk == totalChunks to detect the end of a replay.
hasGap
boolean
Only present when true. Set on the first chunk when the cache didn't cover the entire disconnect period, and repeated on the terminal chunk when the replay had to stop early. 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 hasMore (or 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-replayhasMore: truemeans another chunk follows; the terminal chunk hashasMore: falseandchunk == totalChunkshasGapis set on the first chunk (chunk: 1), and repeated on the terminal chunk if the replay stopped earlyProcess chunks in order as they arrive
Important: Replay messages do NOT have a seq field. Only live event messages carry the per-subscription sequence. Distinguish replay from live by "type": "replay".
Connection Flow
Initial Connection
Server responds with connected:
Save the sessionId - you'll need it for reconnection.
Subscribing
Server confirms with subscriptionUpdate:
requestId is optional, but a unique value is recommended for every subscribe or unsubscribe request. When present it must be at most 128 characters and must not contain control characters; invalid values are rejected with code: "invalid_request_id". The server echoes valid ids on both confirmations and errors, allowing multiplexed clients to match feedback to the request that caused it. Unsubscribe confirmations additionally contain unsubscribed. These correlation fields apply to the request/response exchange only; live data frames do not carry requestId.
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. Route using the additive channels array, which contains canonical client subscription types. The legacy subscriptions array remains available and may contain internal all-market policy keys such as allFillsAll or l2BookAll.
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 replay coverage retained by the server - some events aged out before you reconnected. hasGap is determined from block coverage, not from individual events, so an item-time ordering violation and hasGap are separate signals and should be diagnosed independently.
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):
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
Why Overlap Can Still Happen
The server adds you to live broadcast immediately, then holds those frames until recovery completes. Held live that the recovery watermark already covers is dropped. A live batch that straddles the last replayed item is delivered whole, so earlier items in that batch can duplicate replay.
allMids is snapshot-only and does not advance the shared session cursor. Per-reconnecting-client memory is bounded at the client channel plus the hold queue (2× WS_CLIENT_BUFFER_SIZE).
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
builderApprovedFundings
(time, user, coin)
entry.time, entry.user, entry.delta.coin
builderApprovedNonFundingLedgerEvents
(time, txIndex, user, role)
entry.time, entry.txIndex, entry.user, entry.role
Deduplication Example (Fills)
Since keys are monotonically increasing, track the last processed (time, txIndex) and skip events at or before it:
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 (opaque string)
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
Last updated