1. Getting Started
wss://realtime.insightsentry.com/livewss://realtime.insightsentry.com/newsfeedCLI Quick Start
The insight CLI streams messages as NDJSON:
npm install -g @insightsentry/mcp
# Quotes and series for multiple symbols
insight stream_live --symbols "NASDAQ:AAPL,NASDAQ:MSFT" --type both \
--bar_type minute --bar_interval 1 --max_messages 100 --duration_seconds 30 --quiet
# Append news messages to a file for 30 minutes
insight stream_newsfeed --content "earnings" --output_file ./news.ndjson --append \
--duration_seconds 1800Run without flags for guided setup. See Insight Tool for authentication, output, and duration options.
AI Agent Collection
For AI clients, use collect_live or collect_newsfeed. See Insight Tool for arguments and response fields.
WebSocket Transport
Both endpoints use the standard HTTP/1.1 WebSocket Upgrade. Most WebSocket libraries handle the upgrade automatically. If a proxy sits between your client and the API, it must preserve the WebSocket upgrade.
Authentication
Authenticate with your dashboard API key. The request format differs by endpoint:
Market Data Authentication
Combine authentication with your subscription request (covered in the next section).
News Feed Authentication
Send only your API key in this simplified format:
{
"api_key": "<your_websocket_api_key>"
}News Feed Instant Access
After the request becomes active, /newsfeed replays up to 10 of the most recent matching news items, then streams new matching items. Optional filters are documented in Section 2.
Connection Lifecycle & Limits
Open the socket, then send the endpoint's authentication/subscription JSON within five minutes. While an accepted request is being connected, either endpoint can send:
{
"event": "connection_status",
"status": "connecting",
"message": "Connecting..."
}This is progress information, not proof that the subscription is ready. Wait for the following message before treating the request as active:
{
"event": "subscription_status",
"status": "active",
"operation": "initial",
"subscription_count": 2
}operation is initial for the first accepted request and update after a replacement. For market data, subscription_count counts active quote/series stream legs, so one both request counts as two. Newsfeed reports one active subscription.
- Initial request timeout: You must send a valid subscription message within 5 minutes of connecting. Idle connections that never authenticate are closed automatically.
- Connection limit: You can keep up to your plan's allowed concurrent connections open per account. Market data may replace an older local connection and send it
connection_evicted. Otherwise, including when the older connection is on another server, the new socket is rejected withmax_connection_reached. Newsfeed rejects the new socket when the limit is reached. See Section 6 for details.
2. Subscribing to Data Feeds
Market Data Subscription
After connecting to the market data endpoint, send a JSON message to authenticate and subscribe to data feeds.
Required Message Format:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "quote"}
]
}Optional Adaptive Delivery
Add slow_consumer_policy: "adaptive" at the top level if preserving the connection with fresh, lower-frequency market data is preferable to receiving every intermediate update. Without it, or with "disconnect", the existing close-on-backlog behavior remains in effect.
{
"api_key": "<your_websocket_api_key>",
"slow_consumer_policy": "adaptive",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "quote"}
]
}Every accepted subscription message replaces the complete subscription set and selects the delivery policy again. Include slow_consumer_policy: "adaptive" on replacement requests to keep adaptive delivery enabled. A rejected replacement leaves both the previous subscriptions and their delivery policy unchanged.
Subscription Parameters
Each subscription object in the array can include these parameters:
Required Parameters
code- Symbol identifier (e.g.,"NASDAQ:AAPL","BINANCE:BTCUSDT")
Data Type Selection
type-"quote","series", or"both"
Be explicit: An omitted or unrecognized type currently behaves as "both" for compatibility. Always send a supported value so a typo cannot create an unintended quote and series subscription.
Series Data Parameters (when type="series")
An explicit type: "series" defaults to one-minute bars when bar settings are omitted. type: "both" defaults its series leg to one-day bars. Send bar_type and bar_interval explicitly when the timeframe matters.
bar_type- Time interval:"tick","second","minute","hour","day","week", or"month"bar_interval- Number of units per bar. Allowed values depend onbar_type:"second":1, 5, 10, 15, 30, 45(SIP:symbols use1)"tick":1"minute": 1–1,440"hour": 1–24"day": 1–365;"week": 1–52;"month": 1–12
currency- Optional. Convert price values to the given currency (e.g.,"USD","EUR"). Unknown codes are rejected.unit- Optional. Override the instrument unit for display (e.g.,"BTC"). Unknown codes are rejected.extended- Non-futures only. Include extended hours (default: true) - details below24h- Series only. Use the 24-hour session on a supported exchange (default: false). It takes precedence overextended- details belowsplit- Non-futures only. Apply split adjustment (default: true) - details belowdadj- Non-futures only. Apply dividend adjustment (default: false). Ignored whensplitisfalse- details belowbadj- Futures only. Apply back-adjustment for continuous futures contracts (default: true) - details belowsettlement- Futures only. Use settlement price as close (default: false) - details belowdp- Number of initial historical data points to receive on connect/reconnect (1-30,000). Default: 1. - details below
Example Subscription Message:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "series", "bar_type": "minute", "bar_interval": 1, "dp": 500},
{"code": "NASDAQ:TSLA", "type": "quote"}
]
}Modifying Subscriptions
Send the complete desired subscription list to replace the current set.
Complete Replacement
Include every symbol you want to keep.
Example - Updating Subscriptions:
To change from the previous example to track both minute bars and quotes for AAPL:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "both", "bar_type": "minute", "bar_interval": 1, "dp": 500}
]
}NASDAQ:TSLA is removed because it is absent from the replacement. If the new streams cannot be activated, the server can keep the previous set active and return subscription_unavailable with subscriptions_unchanged: true. Follow that error's action; do not assume the update took effect.
Subscription Rules & Limits
Required Authentication
Every subscription message must include your valid api_key.
Rate Limiting
Avoid sending more than 120 subscription messages per 3 hours. The server does not drop your messages when you exceed this — instead, it adds small progressive delays to request processing. Frequent reconnections from the same account are throttled the same way to prevent reconnection storms.
Note: Throttling only affects new messages and reconnections you initiate. Your existing data feed continues uninterrupted, and data you receive has no rate limit.
Empty Subscriptions
The initial request must include at least one valid subscription. When you no longer need data, disconnect the WebSocket and reconnect when needed.
Multiple Symbols
Subscribe to multiple symbols in one message by adding multiple objects to the subscriptions array.
How Symbol Quota Is Counted
- Each unique series-only symbol uses one plan unit.
- Up to 10 unique quote-only symbols use one plan unit.
- Each
bothsymbol uses one plan unit and activates two stream legs. - Duplicate stream requests are deduplicated before quota is calculated.
Send Updates Sequentially
Wait for subscription_status or an error before sending the next replacement. A socket permits at most eight pending data requests; exceeding that bound closes the socket with too_many_pending_requests.
Newsfeed Subscription & Filters
For all news, send only api_key. You may also filter by exact source, related symbol, and terms contained in the article content:
{
"api_key": "<your_websocket_api_key>",
"source": "Reuters",
"related_symbols": "NASDAQ:AAPL, MSFT",
"content": "earnings acquisition"
}sourceis a case-insensitive exact match.related_symbolsaccepts comma- or space-separated symbols. A symbol without an exchange prefix, such asAAPL, matches that ticker across exchanges.contentaccepts comma- or space-separated, case-insensitive terms; an item matches when its content contains at least one term.- Different filter fields are combined, so an item must satisfy every populated field.
Sending another valid newsfeed request replaces the filters, emits an operation: "update" status, and replays up to 10 recent items matching the new filters before continuing with live items.
3. Response Data Formats
Messages are JSON objects. Their fields depend on the subscription type.
For complete field descriptions, see the corresponding REST API documentation: /symbols/:symbol/series, /symbols/quotes, or /newsfeed.
Status Messages
The top-level event field identifies market-data updates and connection progress without conflicting with the nested trade-direction type field. Consider the request ready only after subscription_status reports status: "active".
{
"event": "subscription_status",
"status": "active",
"operation": "initial",
"subscription_count": 1
}A market-data request may emit validation errors for individual legs and then an active status for the valid remainder. Use subscription_count to confirm how many stream legs were accepted. If no valid legs remain, the server sends no_valid_symbols and does not send an active status for that request.
Dispatch successful market data using event: "quote_update" or event: "series_update". These discriminators are additive: all existing quote and series fields remain present, so clients that currently recognize messages by shape continue to work while migrating.
If adaptive delivery was requested and the client falls behind, the server sends a delivery status before the compacted market snapshot:
{
"event": "delivery_status",
"mode": "conflated",
"dropped_updates": 42,
"max_rate_hz": 1
}dropped_updates counts skipped updates in this delivery. max_rate_hz is the current delivery rate (1, 2, or 4 Hz). Quote updates combine changed fields; series updates contain the newest complete bar. Initial history is sent separately. A client that cannot keep reading may still be disconnected.
{
"event": "delivery_status",
"mode": "realtime",
"dropped_updates": 0
}mode: "realtime" means full-frequency delivery has resumed.
Series Data (type: "series")
Series data provides OHLCV (Open, High, Low, Close, Volume) bar information and tick data.
Real-time Updates
Regardless of your chosen bar_type and bar_interval, you receive updates whenever the close price or volume changes within the current bar period.
Example: With bar_type: "hour", you get real-time updates throughout the hour, not just once per hour.
Update Frequency by Bar Type
Data delivery varies based on your bar_type subscription:
bar_type: "tick") - Data pushed for every individual tradebar_type: "second") - Data pushed only when close price or volume changesbar_type: "minute" or above) - Data pushed when price/volume changes AND when new bar periods start{
"event": "series_update",
"code": "NASDAQ:AAPL",
"bar_end": 1733432399.0,
"last_update": 1733432399820,
"bar_type": "1m",
"series": [
{
"time": 1733432340.0,
"open": 242.89,
"high": 243.09,
"low": 242.82,
"close": 243.08,
"volume": 533779.0
}
]
}Quote Data (type: "quote")
Quote data provides real-time market information including current prices, trading volume, and bid/ask spreads.
Each WebSocket message is one direct quote object. Quote updates are sparse: fields that are not present in an update may be omitted, so merge updates by code when maintaining a current quote snapshot.
{
"event": "quote_update",
"code": "NASDAQ:AAPL",
"status": "PRE",
"lp_time": 1757061117.0,
"volume": 47549429.0,
"last_price": 239.42,
"change_percent": -0.15,
"change": -0.36,
"ask": 239.47,
"bid": 239.42,
"ask_size": 2.0,
"bid_size": 1.0,
"prev_close_price": 238.47,
"open_price": 238.45,
"low_price": 236.74,
"high_price": 239.8999,
"market_cap": 3558428648118.0,
"currency_code": "USD",
"delay_seconds": 0
}Understanding delay_seconds
The delay_seconds field indicates the data delay in seconds:
0- Real-time with no artificial delay900- Data is delayed by 900 seconds-1- End-of-day (EOD)
Newsfeed Data
News items are sent as direct JSON objects. published_at is Unix epoch time in seconds; the other fields are omitted when unavailable.
{
"link": "https://example.com/news/article",
"source": "Reuters",
"title": "Example company reports quarterly results",
"content": "The company reported its latest quarterly results...",
"published_at": 1757061265,
"related_symbols": ["NASDAQ:AAPL"]
}4. Additional Parameters
See Common Parameters for adjustments and sessions. Subscription examples follow.
Extended Market Hours (extended)
The extended parameter applies to many US and Global stock markets.
extended: true(default) - Includes pre-market and after-hours trading data where available. For US equities, this covers 4:00 AM - 9:30 AM ET (pre-market) and 4:00 PM - 8:00 PM ET (after-hours).extended: false- Only includes regular trading hours data.- Note: Not all markets support extended hours trading. Setting
extended: truefor markets without extended hours will return only regular session data. - Futures subscriptions always include the available extended session.
Example:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "series", "bar_type": "minute", "bar_interval": 1, "extended": false}
]
}24-Hour Session (24h)
Set 24h: true on series subscriptions to request the 24-hour session. For aboth subscription, it applies only to the series leg; the quote leg uses extended hours. Only NYSE, NASDAQ, AMEX, and CBOE are supported. Other exchanges ignore the parameter. Quote-only subscriptions accept the legacy field but treat it as extended: true.
24hdefaults tofalse.- When
24h: trueis effective, it takes precedence overextended.
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "series", "bar_type": "day", "bar_interval": 1, "24h": true}
]
}Split Adjustment (split)
For non-futures instruments, controls whether price history is adjusted for stock splits. This flag is ignored for futures symbols.
split: true(default) - Historical prices are adjusted for splits.split: false- Returns unadjusted prices. When set tofalse,dadjis forced tofalseas well — dividend adjustment requires split adjustment.
Example:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "series", "bar_type": "day", "bar_interval": 1, "split": false}
]
}Dividend Adjustment (dadj)
For non-futures instruments, you can request dividend-adjusted price series. This flag is ignored for futures symbols.
dadj: true- Applies dividend adjustments to all price values. Requiressplit: true(the default).dadj: false(default) - Shows prices without dividend adjustment.
Note: If you send split: false together with dadj: true, the server forces dadj back to false.
Example:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "series", "bar_type": "day", "bar_interval": 1, "dadj": true}
]
}Back-Adjustment (badj)
For continuous futures contracts (e.g., ES1!, NQ1!), you can request back-adjusted price series to remove price gaps between contract rollovers. This flag is ignored for non-futures symbols.
badj: true(default) - Applies back-adjustment to smooth price gaps at contract rollovers.badj: false- Shows unadjusted prices with natural contract rollover gaps.
Example:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "CME_MINI:ES1!", "type": "series", "bar_type": "day", "bar_interval": 1, "badj": false}
]
}Settlement Price (settlement)
For futures contracts, controls whether historical data uses settlement prices for the close value. This flag is ignored for non-futures symbols.
settlement: true- Uses settlement prices as the close price where applicable.settlement: false(default) - Uses the regular close price (last traded price).
Example:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "CME_MINI:ES1!", "type": "series", "bar_type": "day", "bar_interval": 1, "settlement": true}
]
}Initial Data Points (dp)
Control how many historical data points you receive when first connecting or reconnecting to a symbol. The dp parameter accepts a number from 1 to 30,000 and defaults to 1. Values above 1 round up to the next 500.
When you connect (or reconnect), the first series data message contains the most recent dp bars in the series property, followed by real-time updates with single bars.
- Plans with at least 10 symbol units: up to 30,000 data points.
- Other Plans: up to 1,000 data points.
- If you specify a value higher than your plan allows, it will be automatically clamped to your plan's maximum.
Example:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "series", "bar_type": "minute", "bar_interval": 1, "dp": 500}
]
}5. Maintaining Connection & Best Practices
Re-subscribe and Wait for Activation
On Every Open
Send the complete authentication/subscription request after each successful WebSocket open. Do not mark the connection ready merely because the upgrade or request validation succeeded. Wait for subscription_status with status: "active", and keep processing errors while activation is pending. Use a finite activation deadline (the example uses five minutes) so an open-but-never-active client cannot wait forever.
Control Ping/Pong
Both endpoints send a WebSocket control Ping every 10 seconds. Your client must return a control Pong before the next deadline or the server closes the socket with heartbeat_timeout. Mature WebSocket libraries reply automatically; keep that behavior enabled and avoid blocking the library's I/O loop.
Optional Application Ping
You may send the text message ping and expect the text response pong. This is optional and does not replace control Pong handling. If you use it for application-level checks, once every 20–30 seconds is sufficient.
Market-Data Server Time
After a /live subscription is active, the server sends a JSON timestamp about every 15 seconds. /newsfeed does not send this message.
Message format:
{"server_time": 1741397070281}The value is Unix epoch time in milliseconds. Use it to estimate delivery lag, allowing for clock skew. Do not use the absence of market updates as a liveness signal because markets can be inactive.
Read Continuously
Keep reading while processing data separately. Slow clients may be disconnected with slow_consumer. Adaptive delivery can skip intermediate updates, but does not replace continuous reading.
Reconnection Best Practices
Reconnect after transient failures using bounded exponential backoff.
Use Exponential Backoff with Jitter
Start near 500 ms, double after each consecutive failure, add random jitter, and cap the delay around 30 seconds. Reset the attempt counter only after the subscription is active and the connection remains healthy for a stability window. Set a finite retry budget, such as 10 attempts, then surface the failure to your application. Fast reconnect loops trigger progressive delays and can make recovery slower.
| Server instruction | Client behavior |
|---|---|
action: "wait_and_retry" | Keep the socket open. For processing/reconnect throttles, the current request resumes automatically after delay_seconds. For an HTTP 429, wait at least Retry-After before opening a new socket. |
action: "retry_request" | Keep the socket and current subscriptions. Retry the replacement with bounded exponential backoff and a finite attempt limit; subscriptions_unchanged confirms the old set remains active. |
action: "reconnect" | Close or confirm closure of the old socket, then reconnect with backoff and send the complete subscription again. |
action: "reconnect_after_fixing_consumer" | Stop the reconnect loop. Restore or replace the stalled data consumer first, then reconnect with backoff. |
retryable: false | Follow the specific action—fix the request, update credentials, close another connection, or contact support—before attempting the same operation again. |
Recommended Reconnection Flow
- Read any structured error and close event; choose behavior from its action.
- Close or confirm closure of the old socket, then clear its timers, retry tasks, and listeners.
- Wait for the larger of the server-provided delay and your jittered exponential backoff.
- Open one new connection and send the complete subscription request once.
- Reset the attempt counter after
subscription_status: activeand a healthy stability window, not merely after the socket opens.
6. Error Handling
When possible, the server sends a JSON error before rejecting a request or closing the connection. Your client must still handle a close without an error message because the socket may already be unavailable.
WebSocket errors use one common envelope. Keep the established error value for program logic, follow action for recovery, and include the opaque reference_id when contacting support.
{
"error": "invalid_message",
"message": "The message must be a valid JSON subscription request.",
"reference_id": "550e8400-e29b-41d4-a716-446655440000",
"retryable": false,
"action": "fix_request"
}Error Fields
| Field | Description |
|---|---|
error | Stable machine-readable error code. Existing values are preserved across message improvements. |
message | Safe, user-facing explanation. Do not parse this field for program logic. |
reference_id | Opaque identifier to include in a support request. It does not identify a symbol, session, or server. |
retryable | Whether retrying is appropriate after following the requested action. Do not immediately retry every error marked true. |
action | Recommended recovery, such as fix_request, update_credentials, wait_and_retry, or reconnect. Subscription updates can also use retry_request. |
| Optional fields | Some errors add symbol, code, details, delay_seconds, retry_after_seconds, or subscriptions_unchanged. |
Common Examples
An invalid or outdated dashboard WebSocket API key:
{
"error": "invalid_apikey",
"message": "The API key is invalid or no longer current. Check the current WebSocket API key in your dashboard.",
"reference_id": "550e8400-e29b-41d4-a716-446655440000",
"retryable": false,
"action": "update_credentials"
}A temporary subscription failure that requires a reconnect:
{
"error": "subscription_unavailable",
"message": "One or more requested subscriptions are temporarily unavailable. Please reconnect. If this persists, contact support with the reference ID.",
"reference_id": "550e8400-e29b-41d4-a716-446655440000",
"retryable": true,
"action": "reconnect"
}A validation error can include the affected symbol while the socket stays open:
{
"error": "invalid_bar_interval",
"message": "Invalid bar_interval for second interval. Allowed values: 1, 5, 10, 15, 30, 45",
"symbol": "NASDAQ:AAPL",
"reference_id": "550e8400-e29b-41d4-a716-446655440000",
"retryable": false,
"action": "fix_request"
}Error Codes
error | Endpoint | Meaning | Recovery |
|---|---|---|---|
invalid_message, invalid_subscriptions | Both; invalid_subscriptions is market data only | The JSON request or market-data subscriptions field is malformed. | Fix and resend on the same socket. |
missing_api_key, invalid_apikey | Both | The API key is missing, invalid, or no longer current. | Update credentials; socket stays open. |
invalid_request | Market data | The WebSocket credential has expired. | Update credentials; socket stays open. |
unsupported_message_type | Both | A binary message was used where a text subscription request is required. | Send text JSON on the same socket. |
websocket_access_required, subscription_expired | Both; subscription_expired is market data only | The account lacks WebSocket access or its market-data subscription expired. | Upgrade or renew before retrying. |
unsupported | Market data | The requested subscription source is not supported by this endpoint. | Fix the request; socket stays open. |
invalid_symbol, invalid_bar_interval, invalid_currency_code, invalid_unit_code | Market data | One subscription leg failed validation. Other valid legs may still be accepted. | Fix the affected symbol and resend if needed. |
no_valid_symbols, subscription_limit_exceeded | Market data | No valid symbols remain or the request exceeds the plan limit. Limit errors add requested and maximum counts under details. | Fix the request; socket stays open. |
quote_error, series_error | Market data | A requested symbol or series could not be activated. The requested code is included in code. | Follow the message and correct the request. |
subscription_unavailable | Both | A requested or active subscription could not be maintained. An update may add subscriptions_unchanged: true. | Follow action; the server may keep an unchanged update open or close and request a reconnect. |
processing_throttled, reconnect_throttled | Both | The server delayed the request because activity or reconnections are too frequent. The response includes delay_seconds. | Wait; the current request resumes automatically. |
max_connection_reached | Both | The account reached its concurrent connection limit. details includes current and maximum counts. | The rejected socket is closed. |
connection_evicted | Market data | A newer connection replaced this older connection. | Use the new connection; the old socket is closed. |
too_many_pending_requests | Both | Too many subscription requests are concurrently pending on one socket. | The socket is closed; reduce request concurrency. |
heartbeat_timeout, initial_request_timeout | Both | A control pong or valid initial request was not received before its deadline. | The socket is closed; fix handling and reconnect. |
slow_consumer | Both | The client did not read realtime messages quickly enough. Adaptive market-data delivery could not safely compact the remaining backlog, or was not enabled. | The socket is closed; read continuously first. |
internal_error | Market data | The request task could not complete. | Do not retry blindly. Contact support with the reference ID if it persists. |
HTTP 429 Before Upgrade
Excessive connection attempts can be rejected before the WebSocket upgrade. In that case, handle the HTTP 429 Too Many Requests response and its Retry-After header instead of waiting for a WebSocket message.
{
"error": "rate_limited",
"message": "Too many connection attempts were received from this network. Wait 2 seconds before trying again and review your reconnect logic.",
"reference_id": "550e8400-e29b-41d4-a716-446655440000",
"retryable": true,
"action": "wait_and_retry",
"retry_after_seconds": 2
}- Use
error,retryable, andactionfor decisions; treatmessageas display text. - When
actionrequests a reconnect, use exponential backoff and stop retrying after a reasonable client-side limit. - Store the
reference_idwith your own request context for support diagnostics.
7. Code Examples
The examples use Python's websockets library, which answers control Pings automatically.
Before You Start
Install websockets>=14 and set INSIGHTSENTRY_WEBSOCKET_API_KEY to your dashboard API key.
Quick Start
Connect once, wait for activation, and read messages. Add your data handling in the indicated location.
# pip install "websockets>=14"
import asyncio
import json
import logging
import os
import websockets
URI = "wss://realtime.insightsentry.com/live"
ACTIVATION_TIMEOUT_SECONDS = 5 * 60
REQUEST = {
"api_key": os.environ["INSIGHTSENTRY_WEBSOCKET_API_KEY"],
"subscriptions": [
{
"code": "NASDAQ:AAPL",
"type": "both",
"bar_type": "minute",
"bar_interval": 1,
"dp": 500,
},
],
}
def message_summary(message):
if message.get("event") == "series_update":
return f"series update for {message.get('code', 'unknown symbol')}"
if "published_at" in message:
return "news item"
if message.get("event") == "quote_update":
return f"quote update for {message.get('code', 'unknown symbol')}"
return None
async def main():
activated = False
reported_first_data = False
activation_deadline = asyncio.get_running_loop().time() + ACTIVATION_TIMEOUT_SECONDS
async with websockets.connect(
URI,
open_timeout=10,
close_timeout=5,
) as websocket:
await websocket.send(json.dumps(REQUEST))
while True:
timeout = None
if not activated:
timeout = max(
0.0,
activation_deadline - asyncio.get_running_loop().time(),
)
try:
raw_message = await asyncio.wait_for(websocket.recv(), timeout=timeout)
except asyncio.TimeoutError as error:
raise RuntimeError("Subscription did not become active in time") from error
if raw_message == "pong":
continue
try:
message = json.loads(raw_message)
except json.JSONDecodeError:
logging.warning("Ignored an unexpected non-JSON WebSocket message")
continue
if error_code := message.get("error"):
logging.warning(
"WebSocket error=%s action=%s reference_id=%s",
error_code,
message.get("action"),
message.get("reference_id"),
)
if message.get("action") == "wait_and_retry":
continue
return
if (
message.get("event") == "subscription_status"
and message.get("status") == "active"
):
activated = True
logging.info("Subscription active")
continue
summary = message_summary(message)
if summary is not None and not reported_first_data:
logging.info("Received %s", summary)
reported_first_data = True
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())Advanced Reconnect Client
This client separates reads from processing and adds an activation deadline, bounded retries, and error handling. It stops if processing falls behind. Keep handlers asynchronous or offload blocking work.
Full reconnect example
# pip install "websockets>=14"
import asyncio
import json
import logging
import os
import random
import time
import websockets
from websockets.exceptions import ConnectionClosed, InvalidStatus
URI = "wss://realtime.insightsentry.com/live"
API_KEY = os.environ["INSIGHTSENTRY_WEBSOCKET_API_KEY"]
REQUEST = {
"api_key": API_KEY,
"subscriptions": [
{
"code": "NASDAQ:AAPL",
"type": "both",
"bar_type": "minute",
"bar_interval": 1,
"dp": 500,
},
],
}
MAX_ATTEMPTS = 10
BASE_DELAY_SECONDS = 0.5
MAX_DELAY_SECONDS = 30.0
STABLE_AFTER_SECONDS = 60.0
ACTIVATION_TIMEOUT_SECONDS = 5 * 60
DATA_QUEUE_CAPACITY = 1024
PARTIAL_VALIDATION_ERRORS = {
"invalid_symbol",
"invalid_bar_interval",
"invalid_currency_code",
"invalid_unit_code",
}
class StopRetrying(Exception):
pass
class ReconnectRequested(Exception):
def __init__(self, delay_seconds: float, stable: bool):
self.delay_seconds = delay_seconds
self.stable = stable
def number(value, default=0.0):
try:
return max(0.0, float(value))
except (TypeError, ValueError):
return default
def is_stable(activated_at):
return activated_at is not None and time.monotonic() - activated_at >= STABLE_AFTER_SECONDS
async def resend_after(websocket, delay_seconds):
await asyncio.sleep(delay_seconds)
await websocket.send(json.dumps(REQUEST))
def enqueue_data(data_queue, message):
if (
message.get("event") not in ("quote_update", "series_update")
and "published_at" not in message
):
return
try:
data_queue.put_nowait(message)
except asyncio.QueueFull as error:
raise StopRetrying(
"local data queue is full; fix the consumer before reconnecting"
) from error
async def consume_data(data_queue):
processed_messages = 0
while True:
message = await data_queue.get()
try:
# Replace this counter with your storage or processing call.
# Await async work here, or use asyncio.to_thread for blocking work.
processed_messages += 1
if processed_messages == 1 or processed_messages % 1000 == 0:
logging.info("Processed %d data messages", processed_messages)
finally:
data_queue.task_done()
async def connect_once(data_queue):
activated_at = None
retry_task = None
request_retry_attempt = 0
request_needs_fix = False
activation_deadline = time.monotonic() + ACTIVATION_TIMEOUT_SECONDS
async with websockets.connect(
URI,
open_timeout=10,
close_timeout=5,
) as websocket:
await websocket.send(json.dumps(REQUEST))
try:
while True:
timeout = None
if activated_at is None:
timeout = max(0.0, activation_deadline - time.monotonic())
try:
raw_message = await asyncio.wait_for(websocket.recv(), timeout=timeout)
except asyncio.TimeoutError:
if request_needs_fix:
raise StopRetrying("activation failed because the request must be fixed")
raise ReconnectRequested(0.0, False)
if raw_message == "pong":
continue
try:
message = json.loads(raw_message)
except json.JSONDecodeError:
logging.warning("Ignored an unexpected non-JSON WebSocket message")
continue
if error_code := message.get("error"):
action = message.get("action")
reference_id = message.get("reference_id")
logging.warning(
"WebSocket error=%s action=%s reference_id=%s",
error_code,
action,
reference_id,
)
if action == "wait_and_retry":
# The current upgraded request resumes automatically.
continue
if action == "retry_request":
request_retry_attempt += 1
if request_retry_attempt >= MAX_ATTEMPTS:
raise StopRetrying(
f"request retry limit reached; reference_id={reference_id}"
)
if retry_task is None or retry_task.done():
if retry_task is not None:
await asyncio.gather(retry_task, return_exceptions=True)
cap = min(
BASE_DELAY_SECONDS * (2 ** (request_retry_attempt - 1)),
MAX_DELAY_SECONDS,
)
delay = max(
number(message.get("delay_seconds")),
random.uniform(cap / 2, cap),
)
retry_task = asyncio.create_task(resend_after(websocket, delay))
continue
if action == "reconnect":
delay = number(message.get("retry_after_seconds"))
raise ReconnectRequested(delay, is_stable(activated_at))
if action == "reconnect_after_fixing_consumer":
raise StopRetrying(
"consumer must be fixed before reconnecting; "
f"reference_id={reference_id}"
)
if action == "fix_request" and error_code in PARTIAL_VALIDATION_ERRORS:
# Other valid legs may still produce an active status.
request_needs_fix = True
continue
if not message.get("retryable", False):
raise StopRetrying(
f"{error_code}; reference_id={reference_id}; action={action}"
)
raise StopRetrying(
f"unsupported retry action={action}; reference_id={reference_id}"
)
if (
message.get("event") == "subscription_status"
and message.get("status") == "active"
):
if activated_at is None:
activated_at = time.monotonic()
if retry_task is not None:
retry_task.cancel()
await asyncio.gather(retry_task, return_exceptions=True)
retry_task = None
request_retry_attempt = 0
request_needs_fix = False
logging.info(
"Subscription active (%s)",
message.get("operation", "unknown"),
)
continue
enqueue_data(data_queue, message)
except ConnectionClosed as closed:
received_close = closed.rcvd
logging.info(
"WebSocket closed code=%s reason=%s",
received_close.code if received_close is not None else None,
received_close.reason if received_close is not None else None,
)
finally:
if retry_task is not None:
retry_task.cancel()
await asyncio.gather(retry_task, return_exceptions=True)
return is_stable(activated_at)
def http_status(error):
response = getattr(error, "response", None)
return getattr(response, "status_code", None)
def http_error_payload(error):
response = getattr(error, "response", None)
body = getattr(response, "body", None)
if not body:
return {}
try:
payload = json.loads(body)
except (json.JSONDecodeError, TypeError, UnicodeDecodeError):
return {}
return payload if isinstance(payload, dict) else {}
def retry_after(error):
response = getattr(error, "response", None)
headers = getattr(response, "headers", {}) or {}
return number(headers.get("Retry-After"))
async def run_connections(data_queue):
attempt = 0
while attempt < MAX_ATTEMPTS:
stable = False
server_delay = 0.0
try:
stable = await connect_once(data_queue)
except StopRetrying:
raise
except ReconnectRequested as requested:
stable = requested.stable
server_delay = requested.delay_seconds
except InvalidStatus as error:
status = http_status(error)
payload = http_error_payload(error)
reference_id = payload.get("reference_id")
logging.warning(
"WebSocket upgrade rejected HTTP %s error=%s reference_id=%s",
status,
payload.get("error"),
reference_id,
)
if status != 429:
raise StopRetrying(
f"WebSocket upgrade failed with HTTP {status}; "
f"reference_id={reference_id}"
)
server_delay = max(
retry_after(error),
number(payload.get("retry_after_seconds")),
)
except Exception:
logging.exception("WebSocket connection failed")
if stable:
attempt = 0
attempt += 1
if attempt >= MAX_ATTEMPTS:
break
cap = min(BASE_DELAY_SECONDS * (2 ** (attempt - 1)), MAX_DELAY_SECONDS)
client_delay = random.uniform(cap / 2, cap)
delay = max(server_delay, client_delay)
logging.info("Reconnecting in %.2f seconds (attempt %d)", delay, attempt + 1)
await asyncio.sleep(delay)
raise RuntimeError("WebSocket reconnect limit reached")
async def run():
data_queue = asyncio.Queue(maxsize=DATA_QUEUE_CAPACITY)
consumer_task = asyncio.create_task(consume_data(data_queue))
connection_task = asyncio.create_task(run_connections(data_queue))
try:
done, _ = await asyncio.wait(
{consumer_task, connection_task},
return_when=asyncio.FIRST_COMPLETED,
)
if connection_task in done:
await connection_task
raise RuntimeError("Connection loop stopped unexpectedly")
await consumer_task
raise RuntimeError("Data consumer stopped unexpectedly")
finally:
for task in (consumer_task, connection_task):
task.cancel()
await asyncio.gather(consumer_task, connection_task, return_exceptions=True)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(run())Using the Same Loop for Newsfeed
Replace the URI and request in either example with the values below. News items have a published_at field instead of a market-data code field.
URI = "wss://realtime.insightsentry.com/newsfeed"
REQUEST = {
"api_key": API_KEY,
"related_symbols": "NASDAQ:AAPL, MSFT",
"content": "earnings acquisition",
}8. FAQ
Q: Why does my WebSocket disconnect frequently?
Check the structured error first. heartbeat_timeout usually means the client's I/O loop could not send a control Pong; slow_consumer means it did not read messages fast enough. Keep I/O separate from heavy processing. If your market-data application prefers fresh snapshots over every intermediate update, enable slow_consumer_policy: "adaptive" and handle delivery_status events. On /live, a consistently large positive difference between your current epoch time in milliseconds and server_time can indicate delayed client handling, network delay, or clock skew. A negative difference usually indicates clock skew, not a blocked thread.
Q: The request passed validation, so why am I receiving no data?
Validation is not activation. Wait for subscription_status with status: "active" and handle any error received first. Once active, quiet markets, unchanged quotes, or restrictive newsfeed filters can legitimately produce no data for a period. If activation never arrives and the socket closes without an error payload, reconnect with backoff and retain the close details for diagnosis.
Q: Why are some one-second bars missing?
A one-second bar is sent only when its close price or volume changes. No message is produced for an unchanged second. If your application requires a continuous timeline, fill missing intervals from the previous bar according to your own normalization rules.