Charting Best Practices
This guide describes the recommended end-to-end process for building charts on top of the market data WebSocket API. It reflects the approach teams use in production for large symbol universes (hundreds to thousands of contracts), and it builds on the chart request and response shapes in the Market Data Request Reference.
At a Glance
- A reliable charting pipeline has four stages: shard your connections, order the incoming data, calculate and warehouse candles, then overlay the live edge.
- Shard a large symbol universe across multiple WebSocket connections so one disconnect takes down only a slice of your charts.
- Each symbol is its own
md/getChartsubscription. Route incoming data by the subscription ID from the acknowledgment, not the request ID. - Buffer and sort ticks before aggregating them. Plotting as-received is the most common source of chart errors.
- Warehouse calculated candles and serve history from storage, so you do not open an exchange subscription per viewer.
Stage 1: Shard Your Connections
A single WebSocket connection can hold subscriptions for many symbols, but putting a large symbol universe on one socket creates a single point of failure and a processing bottleneck: every symbol’s messages serialize through one socket, and one disconnect takes every chart down at once.
Sharding distributes your symbol universe across multiple connections instead. It limits the blast radius of any single disconnect, keeps message processing parallel, and helps you avoid bursting past rate limits when you subscribe to many symbols at startup. (This is distinct from the shardingExpression sharding on the user sync socket, which routes user and account events rather than distributing market data symbols.)
Recommended strategies:
- Fixed-size buckets: assign symbols round-robin so each connection carries a similar count (for example, 50 to 100 symbols per socket).
- Group by product family: keep related contracts together (all ES contract months on one shard, all NQ on another). This makes operational debugging clearer.
- Load-weighted sharding: split by expected message volume rather than symbol count, since actively traded contracts generate far more ticks than thin ones.
- Keep a static symbol-to-socket map so reconnect logic knows exactly which subset to re-subscribe, without re-deriving the scheme.
Routing Data to the Right Symbol
Each symbol subscription is its own message. There is no request body that lists multiple symbols, so subscribing to two symbols on one connection means sending two separate md/getChart messages, each with a unique request ID. Every WebSocket request follows the format (endpoint, request ID, an optional query, and the body, each on its own line):
For example, subscribing to ESU6 as request 101 and NQU6 as request 102:
Each md/getChart acknowledgment echoes your request ID in the i field and returns the subscription ID in both d.historicalId and d.realtimeId:
historicalId and realtimeId carry the same value, so read the subscription ID off the acknowledgment. Build a lookup table from the acknowledgments:
From that point on, every chart data packet arrives with no request ID, tagged only with the subscription ID inside each chart entry:
Match the id field against the lookup table to know which symbol each packet belongs to: here 12432 routes to ESU6 and 12434 to NQU6. Keep both mappings: request ID to symbol (to match acknowledgments) and subscription ID to symbol (to match every message afterward, and to call md/cancelChart later). Two other fields are worth tracking per chart entry:
s: a status tag on the batch.dbmarks bars replayed from historical storage, an empty string marks live bars, andsmarks the in-progress bar at the moment history ends.eoh: sent as its own chart entry (for example,{"id":12432,"eoh":true}) once the historical replay for that subscription ID finishes. This is your per-symbol signal to switch that symbol from backfilling history to overlaying live updates.
Stage 2: Order the Incoming Data
Before you calculate any candle, choose the right data granularity and order what arrives correctly.
Choose raw ticks only if you need custom bars. If standard bar types such as MinuteBar or DailyBar meet your needs, request them directly from md/getChart. This is simpler and avoids the ordering work below. Use raw Tick data only when you need full control over bar construction, for bar types or intervals that md/getChart does not build natively.
Buffer and sort before aggregating. Ticks can arrive out of order: multiple packets per message, overlap at the historical-to-real-time boundary, and normal network batching all contribute. Building candles from an unsorted stream produces gaps and false price spikes. Buffer incoming ticks, sort by timestamp with the tick ID as a tiebreaker, and only then aggregate into candles. Raw ticks also arrive as deltas from a per-packet base price and timestamp rather than absolute values. For how to reconstruct them, see Tick Charts.
Stage 3: Calculate and Warehouse Candles
Rather than re-requesting historical data every time a user opens a chart, calculate candles once and persist them to a warehouse that serves all users.
- Aggregate sorted ticks, or accept pre-built bars from
md/getChart, into OHLCV candles at the interval you need. - Write each closed candle to a database, keyed by symbol and interval.
- Serve historical chart requests from the warehouse rather than from a live exchange subscription. This scales to many concurrent viewers without opening a new subscription per user.
Choosing a Time Range
The timeRange field controls which slice of history a md/getChart request loads. The strategies are mutually exclusive, so pick one:
For example, to load the 100 most recent bars and then stream live:
To load a fixed window, give both ends (closestTimestamp is the most recent end of the range, asFarAsTimestamp is how far back to go):
A chart’s controls map onto these fields: increasing asMuchAsElements loads more bars and decreasing it loads fewer (the zoom control), changing it cancels the old subscription and re-requests, the Size input sets elementSize, and the Type dropdown sets underlyingType.
Backfilling Long History Windows
The data-size limit per request is flexible rather than a fixed hard cap, and it can vary from day to day, but MinuteBar and Tick requests tend to return at most about two months of data per request. To backfill further:
- When the
eohentry arrives for that subscription, find the oldest bar you have gathered. - Issue a new request using that bar’s timestamp as
closestTimestamp, keeping your original target date asasFarAsTimestamp. - Repeat until the oldest bar received reaches your target start date.
For more on this pattern, see How to Handle Data Size Limits.
Stage 4: Overlay Real-Time Data
With history warehoused, the display layer combines two sources:
- Closed historical candles, read directly from the warehouse for fast chart loads.
- The current, still-forming candle, updated live from the real-time stream until it closes. When it closes, write it to the warehouse and begin a new live candle.
This gives users fast-loading historical charts with a live-updating right edge, without keeping an exchange subscription open per viewer for historical data.
Best Practices Checklist
The core idea across all four stages is the same: do not make the exchange subscription do double duty as your storage and distribution layer. Shard connections so any single socket stays resilient, calculate candles once from correctly ordered data, warehouse the result, and overlay only the live edge on top.