Choose a Stock API by Testing Disconnect Recovery, Not Just Streaming Speed
A stock API can reconnect quickly while leaving an application with incomplete subscriptions or an unexplained gap in event history. Choose it by testing when data becomes usable again, not merely when the socket opens.
A fresh snapshot can restore current state; only a documented replay or backfill mechanism can establish the required event continuity. The acceptance test below separates those outcomes and includes a small, locally tested recovery gate.
Start the comparison at the point where the connection breaks
A fast first message is useful, but it answers a narrow question. After a network interruption, the same client may connect successfully, receive a heartbeat, and still be missing one of the subscriptions its application needs. Alternatively, all subscriptions may return, yet trades that happened during the interruption remain absent. These are different failures.
A price display may recover once it has a valid current quote for each required instrument. A trade-derived calculation may need every event in the missing interval, with duplicates handled correctly. Do not give both applications the same definition of βrecovered.β
For an API selection trial, write down two acceptance contracts before testing. The state contract specifies which instruments and fields must be restored, how freshness is judged, and whether partial availability is allowed. The history contract specifies whether missing events must be recovered, which identifier orders them, and how the application handles an interval it cannot reconstruct.
An API can pass the first contract and fail the second without contradicting its documentation. This is an evaluation method, not a ranking or a live vendor benchmark. The code below was tested with synthetic events; no provider recovery timings were measured.
Give recovery more than one finish line
Record the failure injection, the moment the client detects it, the new connection opening, subscription confirmation, reconciliation completion, and the point at which the application releases usable data. Use a monotonic clock for these local durations. Keep provider event timestamps separately; subtracting unrelated clocks can turn clock skew into an apparent latency result.
Measure at least three intervals:
- Detection delay: failure injection to the client marking the session unavailable.
- Transport recovery: failure injection to the replacement connection opening.
- Usable-data recovery: failure injection to the application meeting its state and, where required, history contract.
If you also report recovery after the network is restored, label that start point separately. A forced ten-second outage should not be compared with a one-second outage as though they were the same test. Runs that never recover within the observation window remain failures or censored observations, not missing rows discarded from a latency average.
Figure 1. An illustrative client recovery model. Opening a connection is only one gate; this is not a vendor protocol state machine.
A successful subscription response is evidence about a subscription request, not proof that its data has arrived. Keep the desired subscription set outside the socket object, then compare it with the evidence available on the new connection. Track this per channel and instrument when the application depends on several streams. If one subscription is rejected, leave that portion unavailable rather than promoting the whole session to healthy.
A heartbeat belongs to the transport check. TickDB's Ping documentation, for example, describes a ping/pong exchange that checks connection availability. Its timestamp is not a quote timestamp, and receiving a pong does not establish that a requested stock subscription is active or that a missed trade has been recovered.
The same distinction prevents a misleading timeout rule: a quiet instrument is not automatically a broken connection. Freshness policy must respect the market session, instrument activity, and the meaning of the channel. A newly received snapshot can legitimately contain an old last-trade timestamp. If the application needs a recent trade, that instrument may remain unusable even though transport and snapshot delivery are working correctly.
Ask exactly what happens to the missing interval
Suppose the client receives event E1, disconnects while E2 and E3 occur, and reconnects before E4. A current snapshot may provide the state after E4. It does not reveal the path through E2 and E3. That distinction matters for volume accumulation, event-driven signals, and audit records, even when the displayed price looks correct.
Figure 2. Illustrative recovery semantics. Replay can repair event history only when the source supports the required interval and ordering; a snapshot alone cannot prove continuity.
For snapshot recovery, ask how the client recognizes the complete required state. Is there a completion marker, a stable expected symbol set, or an explicitly documented reconciliation procedure? A batch-size limit is not a completion signal. Nor is βno messages arrived for a moment.β
TickDB's US Full-Market Ticker documentation provides a concrete example of why these questions matter: subscribing to US_Stock sends a batched initial snapshot, then changed tickers, with at most 500 ticker messages per array. Its Best Practices page says to resend subscriptions after reconnect and that a full-market subscription receives another initial snapshot. The reviewed pages do not establish a replay cursor or an explicit snapshot-end marker. That is a documentation boundary to resolve for the intended application, not proof that those features cannot exist.
For an event-history contract, ask for the actual replay boundary: what cursor or event identifier can be resumed, how long events are retained, whether limits apply per instrument or connection, and what happens when the requested point is too old. A timestamp alone may not uniquely identify events. Do not invent deduplication using price and time unless the feed guarantees that combination is unique.
Also resolve the join between backfill and the resumed live stream. Buffering new events while fetching history is useful only when a documented ordering or watermark lets the client merge them correctly. Without that contract, a gap or a duplicate at the seam can survive a successful backfill request. Mark continuity as unverified instead of claiming that two successful requests prove it.
Test the gate independently of the network client
The following standard-library Python example demonstrates an application-level gate for a finite set of required instruments. It is deliberately not a WebSocket client or an implementation of replay. The adapter must supply subscription evidence and an explicit reconciliation decision. In production, those decisions need to come from the provider's documented semantics and the application's acceptance policy.
The generation number rejects callbacks left over from an older connection. Every reconnect starts with empty evidence, and a heartbeat cannot make the gate usable. The example uses local receipt age only; provider event-age validation belongs in the adapter before accepting a record.
from dataclasses import dataclass , field
@dataclass
class RecoveryGate :
required : frozenset [ str ]
max_age : float
generation : int = 0
connected : bool = False
subscribed : bool = False
reconciled : bool = False
received : dict [ str , float ] = field ( default_factory = dict )
def begin ( self ) :
self . generation += 1
self . connected = True
self . subscribed = self . reconciled = False
self . received . clear ( )
return self . generation
def accept ( self , generation , kind , now , symbol = None ) :
if generation != self . generation or not self . connected :
return
if kind == "disconnect" :
self . connected = self . subscribed = self . reconciled = False
self . received . clear ( )
elif kind == "subscription_ok" :
self . subscribed = True
elif kind == "subscription_error" :
self . subscribed = self . reconciled = False
self . received . clear ( )
elif kind == "record" and self . subscribed :
if symbol in self . required :
self . received [ symbol ] = now
elif kind == "reconciled" and self . subscribed :
self . reconciled = True
# Heartbeats deliberately do not advance recovery.
def usable ( self , now ) :
return ( bool ( self . required ) and self . connected and self . subscribed and self . reconciled and all ( s in self . received and 0 <= now - self . received [ s ] <= self . max_age for s in self . required ) )
gate = RecoveryGate ( frozenset ( { "AAA" , "BBB" } ) , max_age = 5.0 )
old = gate . begin ( )
gate . accept ( old , "disconnect" , 0.0 )
new = gate . begin ( )
gate . accept ( new , "heartbeat" , 1.0 )
assert not gate . usable ( 1.0 )
gate . accept ( old , "subscription_ok" , 1.0 ) # Stale callback.
assert not gate . subscribed
gate . accept ( new , "subscription_ok" , 1.0 )
gate . accept ( new , "record" , 1.0 , "AAA" )
gate . accept ( new , "reconciled" , 1.0 )
assert not gate . usable ( 1.0 ) # BBB is still missing.
gate . accept ( new , "record" , 2.0 , "BBB" )
assert gate . usable ( 2.0 )
assert not gate . usable ( 7.0 ) # AAA has aged out.
gate . accept ( new , "subscription_error" , 7.0 )
assert not gate . usable ( 7.0 )
print ( "Synthetic recovery-gate checks passed" )
Run this as a Python 3.9-or-newer script. AAA and BBB are synthetic instrument labels, and five seconds is an illustrative receipt-age threshold, not a vendor SLA. The expected output is βSynthetic recovery-gate checks passed.β
The small gate assumes one aggregate subscription decision and serialized callbacks. A real adapter may need per-channel decisions, locking, bounded buffers, and schema checks. For an event-dependent application, do not emit βreconciledβ until the missing interval and the live/backfill seam satisfy the history contract. For a display, reconciliation may be a narrower current-state decision. Keep those policies explicit rather than embedding them in the reconnect handler.
Run a failure-injection trial that can actually reject a candidate
Use the same required instruments, market session, channel mix, account permissions, client region, and outage durations for each candidate. Record the client version and subscription request so a change in the harness does not masquerade as a provider improvement. Test only connections you control, within service limits; do not generate disruptive load against a provider.
Start with four cases, because each challenges a different claim:
- Clean disconnect. Close the socket, reconnect, and confirm that the desired subscriptions are restored. The pass condition is usable data under the declared contract, not a successful open callback.
- Silent interruption. In a test harness or permitted local network proxy, stop delivery without a clean close. Verify that the client eventually detects the loss and stops presenting the old session as usable. Keep a fixture that delivers heartbeats but withholds records to test the separate data gate.
- Partial recovery. Reject one requested subscription or withhold one required instrument in the fixture. The application must report the missing coverage and remain wholly or partially unavailable according to its predeclared policy.
- Recovery race. Deliver a delayed callback from the previous connection, repeat an event, and interrupt again during reconciliation. Confirm that old-generation callbacks cannot update the new session and that any event deduplication uses a documented identity rule.
Use local fixtures to prove that the client handles these conditions; use permitted live trials to learn how a candidate behaves. A synthetic rejection is evidence about the harness and client, not evidence that a provider produces that rejection. Record those two evidence types separately.
Include a configuration failure as a separate check. For example, TickDB documents mandatory permessage-deflate negotiation for full-market subscriptions, with error 2008 when it is missing, and permission error 4005 when the key lacks access. Retrying the same invalid configuration does not demonstrate resilience. The trial should verify that the client exposes an actionable error instead of looping indefinitely. Follow the selected provider's documented retry policy and stop conditions rather than applying one universal schedule.
Make the selection from acceptance results, then compare speed
For each candidate, keep one record per injected failure: scenario, session and instruments, detection delay, transport recovery, usable-data recovery, missing subscriptions, unresolved history interval, and final outcome. Label every result as documentation-based, synthetic-test evidence, live observation, or unknown. Report the observation window and failures alongside any latency summary. With a small sample, show the individual runs instead of presenting a percentile as a stable service property.
A current-state application can accept snapshot recovery if it meets its coverage and freshness requirements. An event-dependent application needs a credible way to reconstruct the missing interval, or an explicitly accepted degraded mode that stops the affected calculation. Neither decision should be hidden inside a green connection indicator.
Reject a candidate that cannot satisfy the application's recovery contract, even if its first message arrives sooner. Among candidates that pass, compare usable-data recovery time, operating effort, and the rest of the commercial requirements. That ordering makes streaming speed useful without allowing it to conceal a broken recovery path.
Documentation basis: TickDB Best Practices, Ping, and US Full-Market Ticker, reviewed for this article. Product statements are documentation-based; the executable example uses synthetic data only.
Comments
No comments yet. Start the discussion.