At 03:17 UTC, an order book can vanish between two consecutive polls. A marketable bid disappears, spreads widen by an order of magnitude, and a strategy that appeared liquid at 12:45 becomes unable to execute without crossing the book. No single API response explains the failure. The real question is whether the feed remained current, whether the timestamps remained aligned, and whether the client recovered before the next liquidity event.
The 2025 Bitcoin stress episodes exposed this distinction. The widely reported October selloff produced sharp intraday losses and exceptional liquidation volume on major venues, although the exact decline varies with venue, contract, timestamp, and whether the measurement is based on a daily close or an intramonth peak-to-trough window. Treating “BTC fell 30% in one day” as a universal event obscures the engineering problem: each data source experiences the same market shock through a different path.
For technical teams, the useful question is therefore not “which provider survived the crash?” but “how should we measure survival?” This article defines a reproducible stress-testing framework for historical replay, live latency, disconnection recovery, sequence integrity, and SLA validation across cryptocurrency data feeds.
1. Define the Event Before Testing the Feed
A market event and a data event are not equivalent. The market shock provides the workload; the feed test determines whether the application received a sufficiently faithful representation of that workload.
A cross-provider test must specify the event along five dimensions:
| Dimension | Example specification | Why it matters |
|---|---|---|
| Instrument | BTC perpetual or spot | Different venues expose different symbols and contracts |
| Observation window | T−30 min to T+60 min | Establishes pre-event baselines and recovery behavior |
| Price shock | Synthetic −30% close-to-close path | Standardizes the scenario across providers |
| Execution venue | Binance, OKX, Coinbase, or a selected venue | The source and destination may use different liquidity |
| Event clock | Venue exchange timestamp, UTC | Prevents local receipt time from defining the event incorrectly |
The October 2025 market stress should be represented by two related fixtures:
- Observed replay: use timestamped trades, quotes, and depth updates from a specific venue during the actual stress window.
- Synthetic −30% shock: use a normalized price and liquidity path that imposes a standardized workload across providers.
These fixtures answer different questions. The observed replay tests whether the provider retained a historically verifiable market record. The synthetic shock tests whether the live pipeline can absorb a defined magnitude of volatility, disorder, and disconnection.
A cross-provider comparison must also avoid comparing unlike assets. A feed that carries BTC perpetual swaps should not be compared directly with a source carrying spot BTC, because the instruments have different participants, liquidity, and price formation.
The critical measurement distinction
The interval between an exchange event timestamp and a local receipt timestamp contains several components:
Total observed lag
= exchange clock offset
+ venue publication delay
+ vendor normalization delay
+ network transit delay
+ client deserialization delay
+ application queue delay
Only the first-order components can be controlled during a test. Exchange and local clocks must therefore be synchronized, preferably with NTP or PTP, and the measurement environment must record both wall-clock and monotonic timestamps.
A large exchange_timestamp - receipt_timestamp value does not automatically prove that the provider was slow. Clock offset, a coarse timestamp unit, and deliberately delayed market data can all produce the same symptom.
2. Failure Modes Under Extreme Crypto Volatility
Extreme volatility increases both message rate and the probability that a feed changes its behavior. The following failure modes matter more than ordinary packet loss.
| Failure mode | Market condition that triggers it | Observable symptom | Engineering consequence |
|---|---|---|---|
| Quote staleness | Spread widens and updates arrive slowly | Last quote timestamp stops advancing | Strategy prices against an unavailable market |
| Depth truncation | More levels become visible or liquidity moves away | Order-book depth remains capped or collapses | False liquidity estimates and oversized orders |
| Sequence gap | Venue batches, filters, or drops updates | Client sequence numbers jump | Missing transitions and invalid inferred state |
| Reordering | Concurrent venue workers publish independently | Event timestamps arrive out of order | Incorrect causality and duplicate processing |
| Duplicate delivery | Reconnect replays buffered messages | Same event ID appears more than once | Inflated volume and repeated order actions |
| Clock discontinuity | Venue or provider adjusts its timestamp source | Small negative or large positive lag jumps | Corrupted latency percentiles |
| Rate limiting | Application retries too aggressively | Error code 3001 and Retry-After |
Recovery becomes slower than the original fault |
| Partial outage | Provider retains trades but not depth, or the reverse | Symbol-level data availability changes | Signal calculation silently loses one input |
| False heartbeat | Connection remains open but market data stops | Ping succeeds while event timestamps stagnate | Health checks report a false positive |
| Replay duplication | Client resumes from an inconsistent cursor | Historical window contains repeated segments | Backtest and live data diverge |
Why a WebSocket ping is not enough
A transport-level ping proves that the socket can exchange frames. It does not prove that the upstream market-data publisher is still producing events.
A resilient feed therefore needs two independent health signals:
- Transport health: connection state, WebSocket ping/pong, heartbeat latency.
- Content health: event arrival rate, exchange timestamp advancement, sequence continuity, and symbol-level freshness.
The second signal matters during a crash because an outage can appear as a quiet feed even when the TCP connection remains healthy.
3. Reference Architecture for Multi-Source Validation
A reliable validation environment separates collection, normalization, fault injection, replay, and evaluation. This prevents the test harness from silently repairing the same defect it is supposed to measure.
flowchart LR
A[Venues and Vendors] --> B[Raw Capture Layer]
B --> C[Immutable JSONL Files]
C --> D[Provider-specific Normalizers]
D --> E[Canonical Event Stream]
F[Fault Injector] --> E
E --> G[Replay and SLA Engine]
G --> H[Latency / Gap / Recovery Reports]
G --> I[Strategy and Signal Consumers]
Raw capture layer
The capture layer records the provider payload before normalization. It should preserve:
- Provider and feed identifier
- Instrument and venue
- Local receive timestamp
- Monotonic receive timestamp
- Exchange timestamp, when available
- Provider sequence number or event ID
- Raw event type
- Raw payload
- Connection and subscription session ID
Raw capture is essential because normalization code can accidentally discard evidence—for example, converting a venue-specific timestamp into a misleading default.
Canonical event model
A normalized event should use a stable schema across providers:
{
"source": "provider-a",
"session_id": "session-2025-10-10T00:00:00Z",
"instrument": "BTC-USDT",
"type": "trade",
"exchange_ts_ns": 1759962000123456700,
"receive_ts_ns": 1759962000148200000,
"sequence": 48299131,
"event_id": "provider-a:48299131",
"price": 118245.50,
"size": 0.184
}
The canonical schema should preserve source-specific identifiers rather than replacing them with invented universal sequence numbers. Cross-source matching should use a documented mapping, such as trade ID or a composite event fingerprint, and should never assume that sequence values are comparable between vendors.
Fault-injection layer
The replay engine must be able to introduce deterministic faults:
- Added latency
- Packet or message loss
- Duplicate delivery
- Reordering
- Complete disconnects
- Stale data while the socket remains connected
- Rate-limit responses
- Truncated depth snapshots
- Delayed historical pagination
Deterministic faults make a test repeatable. A random delay without a recorded seed cannot be reproduced during incident analysis.
4. Selecting the Data Sources Under Test
The test should compare data paths, not simply logos. Every provider may aggregate, normalize, cache, or redistribute data from one or more upstream venues.
| Capability | Venue-native WebSocket | Institutional vendor | Public aggregator | TickDB |
|---|---|---|---|---|
| Direct venue timestamp | Usually available | Usually preserved or transformed | May be normalized | Depends on the selected feed |
| Crypto trade data | Supported for supported venues | Supported | Supported | Supported |
| Crypto order-book depth | Venue-defined levels | Plan and venue dependent | Plan and feed dependent | depth supports L1–L10 for crypto |
| Historical OHLCV | Available by venue and retention policy | Often available | Commonly available | Supported through the historical kline endpoint |
| Historical trade retention | Venue dependent | Contract dependent | Contract dependent | Supported for crypto |
| Authentication | Venue-specific credentials | Vendor-specific credentials | Provider-specific credentials | REST uses X-API-Key; WebSocket uses api_key in the URL |
| Fault isolation | Excellent | Provider dependent | Provider dependent | Requires explicit test harness |
| Exact provider latency claim | Must be measured | Must be measured | Must be measured | Must be measured |
This table is a capability map, not a benchmark. Product documentation cannot establish a provider’s p99 latency, recovery time, or loss rate under load. Those values require an SLA validation run against the exact production configuration.
TickDB-specific boundaries
TickDB supports crypto trades and depth data, with crypto depth available from L1 to L10. That boundary matters when defining the test: a ten-level feed must not be compared with a fifty-level provider as though the two snapshots represent equivalent liquidity.
For historical work, the appropriate endpoint is:
GET /v1/market/kline
For a current live candle, use:
GET /v1/market/kline/latest
The two endpoints serve different operational purposes. A backtest should not repeatedly poll a “latest” endpoint, and a live dashboard should not reconstruct its current state from a historical endpoint.
5. Capture Production Data Without Creating Another Fragility Layer
The following collector demonstrates the required resilience patterns:
- Environment-variable authentication
- WebSocket authentication through the URL
- Connection timeout
- Protocol heartbeat
- Application-level
ping - Exponential backoff
- Jitter
- Rate-limit handling for code
3001 - Immutable local capture
- Session identifiers for reconnect analysis
Because subscription messages differ between venues and vendors, the example reads the subscription payload from an environment variable. The collector itself remains provider-neutral while the configuration binds it to a specific feed.
# websocket_collector.py
import asyncio
import json
import os
import random
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
import websockets
API_KEY = os.environ.get("TICKDB_API_KEY")
WS_BASE_URL = os.environ.get("TICKDB_WS_URL")
SUBSCRIPTION_PAYLOAD = os.environ.get("TICKDB_SUBSCRIPTION_PAYLOAD")
OUTPUT_PATH = Path(os.environ.get("CAPTURE_FILE", "raw_capture.jsonl"))
HEARTBEAT_SECONDS = float(os.environ.get("WS_HEARTBEAT_SECONDS", "15"))
MAX_BACKOFF_SECONDS = float(os.environ.get("WS_MAX_BACKOFF_SECONDS", "30"))
RECV_TIMEOUT_SECONDS = float(os.environ.get("WS_RECV_TIMEOUT_SECONDS", "10"))
if not API_KEY:
raise RuntimeError("Set TICKDB_API_KEY before starting the collector.")
if not WS_BASE_URL:
raise RuntimeError("Set TICKDB_WS_URL before starting the collector.")
if not SUBSCRIPTION_PAYLOAD:
raise RuntimeError("Set TICKDB_SUBSCRIPTION_PAYLOAD before starting the collector.")
def utc_now_ns() -> int:
return time.time_ns()
def write_record(record: dict) -> None:
record["local_ts_ns"] = utc_now_ns()
with OUTPUT_PATH.open("a", encoding="utf-8") as output:
output.write(json.dumps(record, separators=(",", ":")) + "\n")
output.flush()
async def application_heartbeat(websocket) -> None:
"""Send TickDB's application-level ping command."""
while True:
await asyncio.sleep(HEARTBEAT_SECONDS)
await websocket.send(json.dumps({"cmd": "ping"}))
async def run() -> None:
retry = 0
while True:
session_id = str(uuid.uuid4())
started_at = datetime.now(timezone.utc).isoformat()
write_record({
"record_type": "connection_attempt",
"session_id": session_id,
"started_at": started_at,
"retry": retry,
})
try:
separator = "&" if "?" in WS_BASE_URL else "?"
url = f"{WS_BASE_URL}{separator}api_key={API_KEY}"
async with websockets.connect(
url,
open_timeout=10,
ping_interval=15,
ping_timeout=10,
close_timeout=5,
max_queue=1024,
) as websocket:
retry = 0
write_record({
"record_type": "connection_open",
"session_id": session_id,
})
subscription = json.loads(SUBSCRIPTION_PAYLOAD)
await websocket.send(json.dumps(subscription))
heartbeat_task = asyncio.create_task(
application_heartbeat(websocket)
)
try:
async for message in websocket:
received_ns = utc_now_ns()
try:
payload = json.loads(message)
except json.JSONDecodeError:
payload = {"raw": message}
code = payload.get("code") if isinstance(payload, dict) else None
if code == 3001:
retry_after = payload.get(
"retry_after",
payload.get("Retry-After", 5),
)
write_record({
"record_type": "rate_limit",
"session_id": session_id,
"code": code,
"retry_after": retry_after,
"payload": payload,
})
await asyncio.sleep(float(retry_after))
raise RuntimeError("Rate limit received; reconnecting.")
write_record({
"record_type": "provider_message",
"session_id": session_id,
"receive_ts_ns": received_ns,
"payload": payload,
})
finally:
heartbeat_task.cancel()
await asyncio.gather(heartbeat_task, return_exceptions=True)
except asyncio.CancelledError:
raise
except Exception as exc:
write_record({
"record_type": "connection_error",
"session_id": session_id,
"error_type": type(exc).__name__,
"error": str(exc),
})
base_delay = min(2 ** retry, int(MAX_BACKOFF_SECONDS))
delay = base_delay + random.uniform(0, max(base_delay * 0.1, 0.1))
retry += 1
write_record({
"record_type": "reconnect_scheduled",
"session_id": session_id,
"delay_seconds": delay,
})
await asyncio.sleep(delay)
if __name__ == "__main__":
try:
asyncio.run(run())
except KeyboardInterrupt:
print("Collector stopped.")
The collector deliberately writes raw messages rather than calculating metrics inside the receive loop. A slow disk or metrics database must not distort the latency of the feed under test.
Operational warnings
- Never place an API key in source control or a captured payload.
- Restrict capture-file permissions because normalized files may contain proprietary order-book information.
- Use a bounded disk reservation and rotate files before the collector runs out of space.
- Avoid running the test from an overloaded laptop; consumer-grade virtualization can dominate the latency distribution.
- Do not use this blocking collector pattern for high-frequency workloads. An
aiohttp- orasyncio-optimized transport is more appropriate when sustained throughput matters.
6. Normalize and Replay the Event
A replay harness should calculate transport metrics without modifying the original capture. It should also distinguish a quiet market from a failed feed by using explicit heartbeat records.
The following dependency-free program accepts normalized JSONL, calculates latency percentiles, identifies duplicate and out-of-order events, measures the maximum heartbeat gap, and applies configurable SLA thresholds.
# replay_sla.py
import argparse
import json
import math
from collections import Counter
from pathlib import Path
def percentile(values: list[float], q: float) -> float | None:
"""Return an inclusive linear-interpolated percentile."""
if not values:
return None
ordered = sorted(values)
position = (len(ordered) - 1) * q
lower = math.floor(position)
upper = math.ceil(position)
if lower == upper:
return ordered[lower]
fraction = position - lower
return ordered[lower] * (1 - fraction) + ordered[upper] * fraction
def load_events(path: Path) -> list[dict]:
events = []
with path.open("r", encoding="utf-8") as source:
for line_number, line in enumerate(source, start=1):
line = line.strip()
if not line:
continue
try:
event = json.loads(line)
except json.JSONDecodeError as exc:
raise ValueError(
f"Invalid JSON on line {line_number}: {exc}"
) from exc
if "exchange_ts_ns" in event and "receive_ts_ns" in event:
event["lag_ms"] = (
event["receive_ts_ns"] - event["exchange_ts_ns"]
) / 1_000_000
events.append(event)
return events
def analyze(events: list[dict], max_heartbeat_gap_ms: float) -> dict:
if not events:
return {"status": "insufficient_data", "event_count": 0}
lags = [event["lag_ms"] for event in events]
event_ids = Counter(
event["event_id"]
for event in events
if event.get("event_id") is not None
)
duplicates = {
event_id: count
for event_id, count in event_ids.items()
if count > 1
}
out_of_order = 0
previous_exchange_ts = None
for event in events:
exchange_ts = event["exchange_ts_ns"]
if (
previous_exchange_ts is not None
and exchange_ts < previous_exchange_ts
):
out_of_order += 1
previous_exchange_ts = exchange_ts
heartbeat_times = sorted(
event["receive_ts_ns"]
for event in events
if event.get("type") == "heartbeat"
)
heartbeat_gaps_ms = []
for previous, current in zip(heartbeat_times, heartbeat_times[1:]):
heartbeat_gaps_ms.append((current - previous) / 1_000_000)
maximum_heartbeat_gap = max(heartbeat_gaps_ms, default=None)
negative_lag_count = sum(lag < 0 for lag in lags)
p95 = percentile(lags, 0.95)
p99 = percentile(lags, 0.99)
checks = {
"p95_lag": {
"value": p95,
"threshold_ms": 250,
"passed": p95 is not None and p95 <= 250,
},
"p99_lag": {
"value": p99,
"threshold_ms": 750,
"passed": p99 is not None and p99 <= 750,
},
"maximum_heartbeat_gap": {
"value": maximum_heartbeat_gap,
"threshold_ms": max_heartbeat_gap_ms,
"passed": (
maximum_heartbeat_gap is not None
and maximum_heartbeat_gap <= max_heartbeat_gap_ms
),
},
"duplicate_events": {
"value": len(duplicates),
"threshold": 0,
"passed": not duplicates,
},
"out_of_order_events": {
"value": out_of_order,
"threshold": 0,
"passed": out_of_order == 0,
},
"negative_lag_events": {
"value": negative_lag_count,
"threshold": 0,
"passed": negative_lag_count == 0,
},
}
failed_checks = [
name for name, result in checks.items()
if not result["passed"]
]
return {
"status": "failed" if failed_checks else "passed",
"event_count": len(events),
"lag_ms": {
"p50": percentile(lags, 0.50),
"p95": p95,
"p99": p99,
"maximum": max(lags),
"minimum": min(lags),
"negative_count": negative_lag_count,
},
"duplicate_event_ids": duplicates,
"out_of_order_count": out_of_order,
"maximum_heartbeat_gap_ms": maximum_heartbeat_gap,
"checks": checks,
"failed_checks": failed_checks,
}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("input", type=Path)
parser.add_argument("--output", type=Path)
parser.add_argument("--max-heartbeat-gap-ms", type=float, default=5000)
arguments = parser.parse_args()
events = load_events(arguments.input)
report = analyze(events, arguments.max_heartbeat_gap_ms)
rendered = json.dumps(report, indent=2, ensure_ascii=False)
if arguments.output:
arguments.output.write_text(rendered + "\n", encoding="utf-8")
else:
print(rendered)
if __name__ == "__main__":
main()
The thresholds in this program are example acceptance criteria, not measured vendor performance. A production SLA should define:
- Which symbol and venue receive the service guarantee
- Whether the guarantee covers receipt at the client socket or availability in the client’s strategy engine
- The percentile window and reporting interval
- The treatment of planned maintenance
- The exchange-event types included in the calculation
- The clock-synchronization assumption
- The maximum acceptable gap, duplicate rate, and recovery time
Validate the harness before trusting the SLA
A test framework can be wrong in a more damaging way than the provider it evaluates. The minimum validation set should include:
- A deliberately delayed event that crosses the p95 threshold.
- A duplicate event with the same event ID.
- Two events delivered in reverse timestamp order.
- A heartbeat interruption longer than the SLA limit.
- A reconnect followed by a repeated historical segment.
- A rate-limit event carrying a
Retry-Aftervalue. - A negative lag caused by clock offset, which must be flagged rather than silently clamped.
7. Historical Replay Is Not Live-Feed Validation
Historical replay answers a different question from live monitoring. It tests whether a stored dataset can reproduce a strategy’s expected decisions; it does not prove how the provider behaved during the original outage.
A historical validation workflow should include:
- Record raw live data during the stress window.
- Download the provider’s historical kline or trade data independently.
- Normalize timestamps, symbols, prices, and sizes.
- Compare bar boundaries and volume aggregates.
- Compare individual trade events where tick-level history is available.
- Quantify missing intervals, duplicate records, and bar-level discrepancies.
- Repeat the strategy against both datasets.
- Record every transformation and exception.
A compact historical retrieval helper for TickDB demonstrates the required authentication, timeout, rate-limit, and error handling:
# fetch_tickdb_kline.py
import os
import time
from datetime import datetime
import requests
API_KEY = os.environ.get("TICKDB_API_KEY")
BASE_URL = "https://api.tickdb.ai/v1/market/kline"
if not API_KEY:
raise RuntimeError("Set TICKDB_API_KEY before running this script.")
def fetch_kline(
symbol: str,
interval: str,
start: str,
end: str,
limit: int = 1000,
) -> dict:
"""Fetch historical candles without embedding credentials in the URL."""
headers = {"X-API-Key": API_KEY}
parameters = {
"symbol": symbol,
"interval": interval,
"start": start,
"end": end,
"limit": limit,
}
response = requests.get(
BASE_URL,
headers=headers,
params=parameters,
timeout=(3.05, 15),
)
try:
payload = response.json()
except ValueError as exc:
raise RuntimeError(
f"Non-JSON response with HTTP status {response.status_code}"
) from exc
code = payload.get("code", 0)
if code == 0:
return payload.get("data")
if code in (1001, 1002):
raise ValueError(
"Invalid or missing API key; check TICKDB_API_KEY."
)
if code == 2002:
raise KeyError(
f"Symbol {symbol} was not found; verify it through "
"/v1/symbols/available."
)
if code == 3001:
retry_after = float(response.headers.get("Retry-After", 5))
time.sleep(retry_after)
raise RuntimeError(
f"Rate limit reached; retry after {retry_after:.1f} seconds."
)
raise RuntimeError(
f"Unexpected TickDB error {code}: {payload.get('message')}"
)
if __name__ == "__main__":
symbol = os.environ.get("TICKDB_SYMBOL")
interval = os.environ.get("TICKDB_INTERVAL", "1m")
start = os.environ.get("TICKDB_START")
end = os.environ.get("TICKDB_END")
if not all((symbol, start, end)):
raise RuntimeError(
"Set TICKDB_SYMBOL, TICKDB_START, and TICKDB_END."
)
result = fetch_kline(
symbol=symbol,
interval=interval,
start=start,
end=end,
)
print(json.dumps(result, indent=2, ensure_ascii=False))
For a robust benchmark, fetch the symbol list first and use the exchange’s exact identifier rather than assuming a separator or settlement format. Historical candles can also reflect vendor-specific aggregation rules, so identical OHLCV rows do not necessarily prove that the underlying event streams were identical.
8. Execute the Fault-Injection Campaign
A credible SLA test uses controlled failures rather than waiting for another market crash.
| Scenario | Injection | Expected client behavior | Primary metric |
|---|---|---|---|
| 500 ms latency spike | Add 500 ms to receipt timestamps | Detect stale data and pause execution | Stale-feed detection time |
| 30-second disconnect | Close the socket | Reconnect with bounded backoff and jitter | Recovery time |
| Duplicate replay | Repeat the last 100 messages | Deduplicate by event ID | Duplicate count reaching strategy |
| Out-of-order delivery | Swap adjacent message order | Reorder within a bounded window | Maximum accepted disorder |
| Stale feed | Keep socket open but suppress events | Heartbeat-based content monitor raises fault | Detection latency |
| Rate limit | Return code 3001 |
Honor Retry-After; avoid a retry storm |
Retry count and recovery time |
| Depth truncation | Remove levels above L10 | Detect level-count change | State consistency |
| Historical API outage | Return HTTP 503 | Retry with bounded exponential backoff | Successful recovery rate |
| Partial outage | Remove trades but retain quotes | Mark instrument health as degraded | Stale-input rejection rate |
The client should support a “fail closed” mode for latency-sensitive execution. When depth exceeds its freshness limit, the strategy should not quietly substitute the last known quote. It should cancel new exposure, reduce order size, or move to a venue-specific recovery path.
Recovery states
A production client should represent feed health explicitly:
HEALTHY
→ DEGRADED elevated latency or sequence gap
→ STALE event timestamp exceeds freshness budget
→ RECONNECTING transport disconnected
→ REPLAYING recovering missed events
→ HEALTHY continuity and freshness checks pass
The transition rules should be part of the strategy specification, not left to individual signal functions.
9. Interpret the Results Without Creating a False Benchmark
Provider rankings based on a single incident are easy to misread. A more defensible report contains five sections.
Coverage
State exactly what was tested:
- Provider and plan
- Venue and symbol
- Order-book depth levels
- Event types
- Test duration
- Network location
- Subscription count
- Concurrent session count
- Clock-synchronization method
Latency
Report p50, p95, p99, and maximum ingestion latency for each event class. Do not report one blended p99 for trades, quotes, and depth if their processing paths differ.
Integrity
Report:
- Duplicate rate
- Out-of-order rate
- Missing-event rate
- Maximum sequence gap
- Maximum quote or depth staleness
- Cross-session replay overlap
Availability
Availability should be measured from the client’s operational perspective:
Availability =
(total test interval - time in an unhealthy or unusable state)
÷ total test interval
A provider may keep the socket open while delivering unusable data. The SLA must therefore define “available” in terms of usable, timely data—not merely an open TCP connection.
Recovery
Report:
- Time to detect disconnection
- Time to establish a new session
- Time to resubscribe
- Time to receive valid market data
- Time to restore a complete strategy state
- Whether historical catch-up was required
10. Deployment Recommendations by Team Type
The correct test design depends on the team’s risk tolerance and operating model.
| User segment | Minimum test scope | Recommended SLA posture | Operational control |
|---|---|---|---|
| Individual researcher | One-minute candles, reconnect test, freshness monitor | p95 historical completion under 5 seconds; hourly retry | Store raw responses and exportable logs |
| Crypto strategy developer | Trades, L1–L10 depth, latency injection, duplicate replay | p95 ingestion under 250 ms; recovery under 15 seconds | Fail closed when depth is stale |
| Production trading team | Multi-venue collection, clock synchronization, packet-loss injection | p99 ingestion under 750 ms; maximum heartbeat gap under 5 seconds | Redundant providers and venue-specific failover |
| Institutional data team | Historical-to-live reconciliation and full fault matrix | Symbol-level availability with documented maintenance treatment | Immutable capture, signed runbooks, quarterly reruns |
| AI-assisted development team | Schema validation and deterministic replay | No undocumented transformations | Versioned adapters and golden datasets |
For an institutional deployment, the SLA should be treated as a testable interface rather than a marketing statement. Store the test configuration, normalized code version, provider plan, venue, and raw artifacts together. Otherwise, a later reviewer cannot determine whether a passing result reflects the same conditions.
11. Common Failure Modes in Data-Source Benchmarks
Comparing different venues
A provider that aggregates one venue may appear slower than a provider connected directly to another venue. That is not a fair provider-level comparison. Use a common upstream event stream or compare the same venue through each delivery path.
Counting client queue delay as network latency
Timestamp the raw receive callback before application processing. If the event enters a large queue before the strategy consumes it, record both values separately.
Ignoring cold starts
A provider may appear fastest during steady state and slowest after a restart. Measure cache warm-up, subscription establishment, snapshot delivery, and incremental updates separately.
Failing to test maintenance windows
An SLA that excludes every maintenance period may be operationally meaningless. Define whether planned maintenance is excluded, capped, or subject to a separate lower service level.
Accepting stale data as valid
A WebSocket connection and a successful heartbeat can coexist with a stalled upstream publisher. The content freshness monitor must reject data that has not advanced.
Rewriting history during normalization
Corporate actions, symbol migrations, contract rolls, and candle aggregation rules can transform historical values. Preserve the original payload and make every transformation visible and versioned.
12. The Result Is a Failure Model, Not a Marketing Leaderboard
The empty order book is only the visible symptom. The deeper failure is a system that keeps acting after its assumptions about time, liquidity, and data continuity have become invalid.
A credible crypto stress test must show how each source behaves under the same event: p95 and p99 ingestion latency, stale-data detection, sequence integrity, disconnection recovery, duplicate handling, and reconciliation between historical and live data. If a provider cannot supply those measurements, its reliability remains an assumption.
The practical conclusion is straightforward: treat SLA validation as an engineering workload with immutable captures, deterministic replay, fault injection, and explicit pass criteria. The result should tell you when to pause a strategy, when to fail over, and when a feed is technically connected but economically unusable.
Next Steps
If you are researching crypto market structure, use the replay harness to compare timestamp quality, gap behavior, and historical reconstruction across providers.
If you are building a live crypto strategy, sign up at tickdb.ai, generate an API key, and set TICKDB_API_KEY before running the REST or WebSocket examples. Crypto trades and L1–L10 depth are available for the relevant feeds.
If you need institutional coverage, historical reconciliation, or multi-venue deployment guidance, contact [email protected].
If you use AI coding assistants, search for the tickdb-market-data SKILL in your tool’s marketplace.
Backtest and SLA limitations: Historical replay results do not guarantee live performance or future provider behavior. The thresholds shown in this article are example acceptance criteria, not measured vendor results. Reconnection tests do not reproduce every form of upstream or network failure, and market-data timestamps remain subject to clock synchronization, aggregation, and venue-specific delivery policies. Validate the complete pipeline against the exact production plan, venue, symbol, and network path before deployment.
This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results.