Skip to main content
Version: 8.6.10.4

MLink API Efficiency Guide

This guide collects the most effective performance tips SpiderRock's Gateway Client Support team has shared with clients integrating against MLink. Each recommendation below has been validated in production with real client workloads — ranging from single-name option chain snapshots to full-universe multi-connection streams — and is accompanied by working code/request examples.

The two most recommended changes are: switch to Protobuf and stop filtering on okey.tk/ticker.tk. Together they are the two highest-leverage changes clients have made to reduce both message size and server-side scan time.


1. Use Protobuf Instead of JSON​

MLink supports both JSON and Protobuf (proto) encodings on REST and WebSocket. Protobuf messages are smaller on the wire and meaningfully cheaper to parse than JSON, which matters most for clients that are CPU-bound on parsing at high message rates (large option chains, full-stripe streaming, etc.). Clients who've made the switch have seen substantially smaller payloads and, in the more demanding streaming use cases, an order of magnitude (~10x) reduction in end-to-end latency versus JSON — because both the encode/decode cost and the bytes-on-the-wire drop together.

REST — JSON vs Protobuf endpoint:

# JSON
GET /rest/json?cmd=getmsgs&msgtype=LiveImpliedQuote&where=okey:eq:SPY-NMS-EQT

# Protobuf (same query, binary response)
GET /rest/proto?cmd=getmsgs&msgtype=LiveImpliedQuote&where=okey:eq:SPY-NMS-EQT

Python — parsing a Protobuf REST/WebSocket response:

SpiderRock publishes ready-to-use Protobuf schema definitions and Python client examples in the Connect SDK:

import requests
from LiveImpliedQuote_pb2 import LiveImpliedQuote # generated from the .proto schema above

resp = requests.get(
"https://mlink-live.nms.saturn.spiderrockconnect.com/rest/proto",
params={
"apikey": API_KEY,
"cmd": "getmsgs",
"msgtype": "LiveImpliedQuote",
"where": "ticker:eq:AAPL-NMS-EQT",
},
)

# Each message in the response body is length-prefixed; parse with the generated class
msg = LiveImpliedQuote()
msg.ParseFromString(message_bytes)

If Framed JSON is being considered as a middle ground, note that the per-frame overhead savings versus standard JSON are negligible — Protobuf is the recommendation whenever parsing/CPU overhead is the bottleneck.


2. Symbol Filtering​

How you scope a query to a subset of symbols has a bigger effect on server-side scan time than almost anything else. There are three patterns, matched to three different use cases.

2.1 WHERE clause: use the full ticker key, not okey.tk/ticker.tk​

Best for: a small, changing set of specific symbols.

Filtering with okey.tk:eq:SSSS or ticker.tk:eq:SSSS forces MLink to scan every record in a message and pattern-match the ticker component, which severely limits query efficiency — this gets worse the more symbols you OR together. Filtering on the full ticker key instead lets MLink jump directly to a pre-indexed bucket for that symbol.

# Slower — requires scanning all records to match the ticker component
GET /rest/json?cmd=getmsgs&msgtype=LiveImpliedQuote&where=okey.tk:eq:AAPL

# Faster — full ticker key, indexed lookup
GET /rest/json?cmd=getmsgs&msgtype=LiveImpliedQuote&where=ticker:eq:AAPL-NMS-EQT

# For messages with no ticker key (e.g. OptionNBBOQuote), use okey instead
GET /rest/json?cmd=getmsgs&msgtype=OptionNBBOQuote&where=okey:eq:AAPL-NMS-EQT

This applies equally to WebSocket where clauses:

{
"header": { "mTyp": "MLinkStream" },
"message": {
"queryLabel": "SPXW_LIQ",
"activeLatency": 1,
"msgName": "LiveImpliedQuote",
"where": "ticker:eq:SPXW-NMS-EQT"
}
}

Note the WHERE clause itself has a hard limit of roughly 10,000 characters / 262,141 bytes — enough for on the order of 1,000 symbols using the full ticker=SSSS-NMS-EQT form. If your universe is larger or changes daily, use a Ticker Basket instead (below).

2.2 Ticker Baskets​

Best for: querying or streaming a larger, well-defined group of symbols (dozens to ~1,000+) without hitting the WHERE-clause size limit.

A Ticker Basket is a named, server-side list of symbols you define once per day and reference in subsequent queries instead of repeating a long WHERE clause. Baskets reset daily and must be redefined each morning. Ticker Baskets work as a parameter on both REST and Websocket requests.

Adding a symbol to a basket uses the same TickerItemDef message shape shown below for removal (see the Ticker Baskets page for the full add/update command); the field you need to know for removal specifically is postaction=D:

Remove an entry from a basket (postaction=D):

POST /rest/json?cmd=postmsgs&postaction=D
{
"header": { "mTyp": "TickerItemDef" },
"message": {
"pkey": {
"ticker": { "at": "EQT", "ts": "NMS", "tk": "AAPL" },
"basketNumber": "0000-1111-2222-3333",
"userName": "YOUR_USERNAME"
},
"clientFirm": "YOUR_CLIENT_FIRM",
"timestamp": "2026-01-01 12:00:00.000000"
}
}

List all currently defined baskets:

GET /rest/json?apikey=YOUR_API_KEY&cmd=getmsgs&msgtype=TickerItemDef

Query using a basket:

GET /rest/proto?cmd=getmsgs&apikey=YOUR_API_KEY&msgtype=LiveImpliedQuoteAdj&tickerbasket=0000-1111-2222-3333&limit=1000000

In head-to-head testing on the same query, a Ticker Basket scanned far fewer records than the equivalent WHERE clause (a measured ~6.5s improvement in filter time on a ~166K message pull: 2.12s for the basket vs. 8.68s for the WHERE clause), because the basket is indexed directly rather than evaluated as a LIKE-style match across the full dataset. Note that on very fast, well-provisioned clients this filtering-time win can be masked by network-layer throttling/flush time — the basket gets the data ready faster, but your own byte-rate limit still governs how fast it can be delivered.

2.3 Stripe Filters​

Best for: pulling the full universe of tickers in a feed.

Stripes are predefined, non-overlapping partitions of the full symbol universe. Instead of one connection scanning and filtering the entire universe, you can split it across multiple connections by stripe, so each connection only has to handle its own partition.

GET /rest/json?cmd=getmsgs&msgtype=LiveImpliedQuote&stripeFilter=A
{
"header": { "mTyp": "MLinkStream" },
"message": {
"queryLabel": "StripeA_LIQ",
"activeLatency": 1,
"msgName": "LiveImpliedQuote",
"stripeFilter": "A"
}
}

For full-universe streaming, clients commonly combine several native sub-stripes into a single compound stripe per connection, then spread those compound stripes across roughly 10 connections total — balancing load without over-provisioning connections:

{ "message": { "msgName": "LiveImpliedQuote", "stripeFilter": "M1|M2" } }
{ "message": { "msgName": "LiveImpliedQuote", "stripeFilter": "M3|M4" } }
{ "message": { "msgName": "LiveImpliedQuote", "stripeFilter": "S|A1" } }

...and so on — each connection assigned its own compound stripe (a union of a few native sub-stripes, e.g. M1–M4, S, A1–H2; see the Stripes page for the full list) until the full universe is covered across roughly 10 connections. This is also the pattern to reach for if you find one connection doing disproportionately more work than the others — we have seen uneven load on a combined "M+S" stripe and resolved it by splitting that stripe across two dedicated connections instead of one.


3. Limit Columns with view​

Every field returned in a message adds to the payload size and therefore to how long it takes to encode, send, and (on the client side) parse. If your application only needs a handful of fields, request only those fields with the view parameter instead of the full message schema.

{
"header": { "mTyp": "MLinkStream" },
"message": {
"queryLabel": "SPXW_LIQ",
"activeLatency": 1,
"msgName": "LiveImpliedQuote",
"where": "ticker:eq:SPXW-NMS-EQT",
"view": "okey|oBid|oAsk|oBidIv|oAskIv|de|ga|th|ve|uPrc|synSpot|sVol|sPrc|sMark|atmVol|calcSource|srcTimestamp"
}
}

This also applies to REST:

GET /rest/json?cmd=getmsgs&msgtype=LiveImpliedQuote&where=ticker:eq:AAPL-NMS-EQT&view=okey|oBid|oAsk|de|ga|th|ve|uPrc

This is one of the recommended levers (alongside more selective WHERE-clause filtering and HighWaterTS, both covered above/below) for reducing overall message size and improving effective throughput when you're near your account's byte-rate limit.


4. Choosing REST vs. MLinkStream vs. MLinkSubscribe​

Best forNotes
REST (getmsgs)A single, consistent point-in-time snapshot — e.g. fitting a vol surface across many legs where every leg must reflect the same market stateIf you're asking "give me everything matching this filter, right now, once" → REST
MLinkStreamContinuous streaming over a WHERE clause, Ticker Basket (via REST/mlink-order), or Stripe Filter — i.e. you don't know the exact symbol list in advance, or it's a large/full-universe pullIf you're asking "keep me updated on this filter/universe forever" → MLinkStream
MLinkSubscribeStreaming a specific, known list of symbols — especially when you need to dynamically subscribe/unsubscribe individual symbols while the stream is runningOnly searches the symbols you've explicitly subscribed to (not the full universe), making it the most efficient option for a small-to-moderate fixed or slowly-changing symbol list. Does not support WHERE-clause filtering directly — identify the pkeys you want and subscribe to them explicitly. Capped at 1,024 pkeys per request (see the MLinkSubscribe schema). Character limit is applied to Views. If you're asking "keep me updated on exactly these N symbols (Ticker Keys or Option Keys), and I'll add/remove symbols as I go" → MLinkSubscribe.

5. Bypassing Cache on Reconnect with highwaterTs​

Messages that carry cached/current state per symbol — such as OptionPrintSet and StockPrintSet — replay that cached state to you on every new connection/subscription before switching to live delivery. MLink signals this with MLinkStreamCheckPt states:

  • Begin — cache replay starting
  • Active — still replaying cached state (this is where the bulk of the "backlog" is)
  • Complete — cache replay finished; everything from here is live

If your use case only needs the latest value going forward (not the full historical/cached state), set highwaterTs to -1. This tells MLink to skip forwarding the Active/cache-replay phase entirely and only deliver messages once the Complete checkpoint is reached — avoiding the wasted bandwidth and startup latency of processing a full cache dump you don't need.

{
"header": { "mTyp": "MLinkStream" },
"message": {
"queryLabel": "OptionPrintSet_<unique_id>",
"activeLatency": 1,
"msgName": "OptionPrintSet",
"where": "prtSize:gt:20",
"highwaterTs": -1
}
}

Two-stream pattern: if you do need the initial cached state and want to avoid it delaying your live data, run two requests — one REST request to consume the initial cache dump, and a second MLinkStream/MLinkSubscribe request with highwaterTs: -1 to receive only live updates without waiting on the first to drain.