REST vs WebSocket: Choosing APIs for Market Data Workflows

A trader opens a dashboard before the market opens, requests the last 2,000 candles, and receives a complete historical series. Thirty seconds later, the same screen updates because a new order book message arrived. The dashboard needs two different behaviors: bounded historical retrieval and continuous real-time delivery.

The distinction matters because a historical query has a finite answer, while a live feed produces an unbounded stream. Treating both as “the same data over the Internet” often creates unnecessary latency, fragile reconnection logic, or systems that cannot recover cleanly after a network interruption.

This article explains when to use REST, when to use WebSocket, and how to design the boundary between them. It also provides production-oriented Python examples, a deployment comparison, and a design for combining historical context with live updates without creating inconsistent state.

1. The Core Design Principle

The correct choice depends on the semantic shape of the data request.

Requirement Preferred interface Reason
Retrieve a bounded historical range REST The request has explicit parameters and a finite response
Retrieve the current candle or latest quote REST The response represents a point-in-time snapshot
Subscribe to continuous market events WebSocket The client maintains a persistent connection and receives updates
Receive periodic snapshots REST Polling provides simple recovery and predictable request volume
Reconcile missed real-time messages REST A fresh snapshot provides a recovery anchor
Stream an unbounded event sequence WebSocket Push delivery avoids repeated polling overhead

REST is request-response communication. A client sends a request, the server returns a response, and the interaction ends. WebSocket is a persistent, bidirectional channel. After the handshake, either side can send messages without opening a new HTTP request for every update.

That architectural difference explains why the interfaces should not be reversed in normal market-data workflows. Historical data is a bounded query, so REST maps naturally to its semantics. Real-time data is an open-ended event stream, so WebSocket maps naturally to that semantic.

A REST endpoint can technically support long polling or server-sent events. A WebSocket can technically request a snapshot. Neither capability makes the alternative design inherently wrong. The question is whether the interface matches the application’s state model, failure model, and traffic pattern.

2. Why Historical Data Naturally Fits REST

A historical request usually has a clear boundary:

symbol + interval + start_time + end_time + limit

The server can validate the request, query a time-indexed store, and return a complete result. The client knows when the operation has finished. This makes REST particularly suitable for:

  • Backtesting across completed historical periods
  • Loading candles before a strategy starts
  • Exporting data for offline analysis
  • Recovering a known time range after an interruption
  • Requesting a current snapshot for a dashboard initialization
  • Integrating with batch jobs and scheduled research pipelines

The response is naturally bounded. A request for 500 one-hour candles should return 500 records, subject to the server’s pagination and limit rules.

A representative historical request looks like this:

import os
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("TICKDB_API_KEY is not configured")

response = requests.get(
    f"{BASE_URL}/AAPL.US",
    headers={"X-API-Key": API_KEY},
    params={
        "interval": "1h",
        "limit": 500,
    },
    timeout=(3.05, 10),
)
response.raise_for_status()

payload = response.json()
if payload.get("code", 0) != 0:
    raise RuntimeError(
        f"Market data request failed: "
        f"{payload.get('code')} {payload.get('message')}"
    )

candles = payload["data"]

The key design choice is the timeout. A market-data request must not wait indefinitely for a server response. A connect timeout of slightly more than three seconds and a read timeout of ten seconds provide a practical balance between failure detection and normal network latency.

Production systems should also handle API-level errors explicitly. The following helper separates successful responses from authentication, symbol, and rate-limit failures:

import os
import time
from typing import Any


def handle_api_error(response, symbol: str | None = None) -> Any:
    """Handle a TickDB JSON response and return its data payload."""
    payload = response.json()
    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!r} was not found. "
            "Verify it through the available-symbols endpoint."
        )

    if code == 3001:
        retry_after = int(response.headers.get("Retry-After", "5"))
        time.sleep(retry_after)
        return None

    raise RuntimeError(
        f"Unexpected API error {code}: {payload.get('message')}"
    )

The 3001 response is not a generic failure. It indicates that the client exceeded a rate limit. The correct behavior is to read the Retry-After header and wait before retrying. Blindly retrying at the same speed can worsen the congestion.

Historical Data Requires a Consistent Boundary

A backtest should not mix completed periods with an endpoint intended to return the current candle. Use the historical kline endpoint for completed historical data, and reserve the latest-candle endpoint for a live dashboard or monitoring service.

That separation prevents a subtle but damaging class of inconsistency: the same bar can change while a backtest is running. A backtest needs a stable input. A live dashboard needs a changing snapshot.

3. Why Real-Time Data Naturally Fits WebSocket

A real-time market feed changes continuously. The client does not know the final size of the response in advance. Polling every second can create three problems:

  1. Unnecessary request volume
    Most responses may contain no meaningful change.

  2. Detection latency
    The newest event is observed only when the next poll completes.

  3. Poor connection economics
    Each poll repeats HTTP request and response overhead.

A WebSocket changes the communication model. The client opens one connection, subscribes to a symbol or channel, and receives messages as they occur.

Client                         Server
  |------- WebSocket handshake --->|
  |<------ connection established --|
  |------- subscribe request ------->|
  |<------ market message 1 ---------|
  |<------ market message 2 ---------|
  |<------ market message 3 ---------|

This model is appropriate for:

  • Live price or candle updates
  • Order book depth snapshots and incremental changes
  • Trade-by-trade events where supported
  • Exchange or market-status notifications
  • Low-latency alert evaluation
  • Maintaining a continuously updated strategy state

The depth channel is particularly well suited to a persistent connection. A client can maintain the current order book state and apply incoming updates instead of repeatedly reconstructing the full book from snapshots.

The available depth coverage is market-dependent: L1 for US equities, L1–L10 for Hong Kong equities and crypto, and no depth support for forex, precious metals, or indices. A client should therefore discover the channel’s actual capabilities rather than assume that every symbol exposes the same number of levels.

Production WebSocket Client

A minimal WebSocket example is easy to write and easy to misuse. A production client must handle heartbeats, reconnects, rate limits, malformed messages, and stale sessions.

import asyncio
import json
import os
import random
import time
from typing import Any

import websockets

API_KEY = os.environ.get("TICKDB_API_KEY")
WS_URL = "wss://api.tickdb.ai/v1/stream"

if not API_KEY:
    raise RuntimeError("TICKDB_API_KEY is not configured")


def parse_message(raw: str) -> dict[str, Any]:
    """Parse and validate a market-data message."""
    try:
        message = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise ValueError("WebSocket returned invalid JSON") from exc

    if not isinstance(message, dict):
        raise ValueError("WebSocket message must be a JSON object")

    return message


async def run() -> None:
    retry = 0
    reconnect_base = 1.0
    reconnect_max = 30.0
    heartbeat_interval = 20.0
    last_ping = time.monotonic()

    async with websockets.connect(
        f"{WS_URL}?api_key={API_KEY}",
        ping_interval=20,
        ping_timeout=10,
        close_timeout=5,
    ) as websocket:
        await websocket.send(
            json.dumps(
                {
                    "cmd": "subscribe",
                    "channel": "depth",
                    "symbol": "AAPL.US",
                }
            )
        )

        while True:
            try:
                raw = await asyncio.wait_for(
                    websocket.recv(),
                    timeout=heartbeat_interval * 2,
                )
                message = parse_message(raw)
                print(json.dumps(message))
                retry = 0

            except asyncio.TimeoutError:
                # The connection is idle longer than expected. Check liveness.
                await websocket.send(json.dumps({"cmd": "ping"}))
                last_ping = time.monotonic()

            except websockets.exceptions.ConnectionClosed:
                delay = min(
                    reconnect_base * (2**retry),
                    reconnect_max,
                )
                jitter = random.uniform(0, delay * 0.1)
                await asyncio.sleep(delay + jitter)
                retry += 1
                break

            except ValueError as exc:
                print(f"Discarded invalid message: {exc}")
                continue

The code illustrates the required failure controls:

  • The API key comes from an environment variable.
  • The WebSocket authentication is passed through the api_key query parameter, as required by the WebSocket authentication model.
  • The client sends a heartbeat message when the connection becomes idle.
  • Reconnection uses exponential backoff.
  • Random jitter prevents multiple clients from reconnecting simultaneously.
  • Malformed messages are discarded without terminating the entire consumer.
  • The client is ready to establish a new session after a closed connection.

For high-frequency production workloads, use a native asynchronous client or an event-driven architecture with explicit backpressure. A standard Python WebSocket loop can be appropriate for dashboards and research systems, but it should not be treated as a complete high-frequency execution platform.

4. REST and WebSocket Are Complements, Not Competitors

The strongest market-data design usually uses both interfaces.

A common pattern separates the system into three layers:

+---------------------------+
|   Client and strategy     |
|   - state reconstruction  |
|   - signal evaluation     |
|   - risk controls         |
+-------------+-------------+
              |
      +-------+--------+
      |                |
      v                v
+------------+   +----------------+
| REST API  |   | WebSocket feed |
| history   |   | live events    |
| snapshots |   | subscriptions  |
+------------+   +----------------+
      |                |
      v                v
+-------------------------------+
| Market-data state manager     |
| - initial snapshot             |
| - event application            |
| - gap detection                |
| - reconciliation               |
+-------------------------------+

Startup Sequence

A reliable client normally follows this sequence:

  1. Request a current snapshot through REST.
  2. Initialize the local state from the snapshot.
  3. Open the WebSocket connection.
  4. Subscribe to the relevant channel.
  5. Process incoming events.
  6. Detect gaps or sequence inconsistencies.
  7. Request a fresh snapshot through REST when recovery is necessary.
  8. Resume event processing after reconciliation.

This sequence prevents a race condition. Without the initial snapshot, live events may arrive before the client has enough state to interpret them.

Recovery Sequence

A reconnect does not mean “continue exactly where the old socket stopped.” A network failure may have hidden messages. The safer recovery path is:

WebSocket disconnect
        |
        v
Pause event application
        |
        v
Reconnect with backoff and jitter
        |
        v
Request a fresh REST snapshot
        |
        v
Replace or reconcile local state
        |
        v
Resume WebSocket processing

The exact reconciliation method depends on the channel’s protocol. Some feeds provide sequence numbers, timestamps, or snapshot identifiers. The application should record those fields when available.

The key principle is that REST provides the recovery anchor. WebSocket provides the low-latency event path.

5. Why the Interfaces Should Not Be Reversed

Why Not Use WebSocket for All Historical Retrieval?

It is technically possible to open a WebSocket and request a historical range, but doing so changes the operational model without necessarily improving the result.

The request still has a finite response. The client still needs to know when the transfer is complete. The server still needs to validate the same parameters. The advantages of REST remain:

  • Natural request-response semantics
  • Straightforward HTTP caching and observability
  • Standard timeout and status-code behavior
  • Easy pagination and retry handling
  • Simple integration with batch and analytical workloads
  • Clear separation between request and response

A WebSocket-based historical request adds persistent-connection management to a process that usually does not need it. It can be justified when the historical response is extremely large, streamed incrementally, or shared across many real-time consumers. That is an architectural decision, not a default.

Why Not Use REST for All Real-Time Delivery?

REST can support near-real-time behavior through frequent polling or long polling, but those approaches introduce a tradeoff between freshness, request volume, and implementation complexity.

Polling every 100 milliseconds may reduce observation latency while multiplying request volume by ten compared with one-second polling. The server also processes repeated requests that often return the same state. A long-polling request keeps a connection open until a change occurs or a timeout expires, but it still requires repeated HTTP negotiation and complicates horizontal scaling.

A WebSocket is a better fit when the server can push updates and the client needs to maintain a continuous state. It reduces repeated request overhead and gives the client a direct event path.

The important distinction is not that REST is “old” or WebSocket is “modern.” The distinction is semantic. REST represents a discrete query; WebSocket represents an ongoing conversation.

6. Interface Design Tradeoffs

Design dimension REST WebSocket
Communication model Request-response Persistent, bidirectional
Natural data shape Bounded range or snapshot Unbounded sequence of events
Connection cost Per request Per session
Failure recovery Retry the request Reconnect and reconcile state
Backpressure Naturally visible at request level Requires explicit buffering or flow control
Observability Standard HTTP logs and status codes Requires connection and message metrics
Authentication Request headers Handshake and session authentication
Caching and pagination Straightforward Application-defined
Browser support Universal Widely supported, subject to environment
Best fit History, snapshots, batch retrieval Live subscriptions and event processing

State and Consistency

REST responses are usually easier to reason about because each request returns a coherent snapshot. WebSocket messages arrive over time, and local state can temporarily reflect a partially applied sequence.

For example, if an order book update arrives while the client is reconnecting, applying it to stale state can produce a negative quantity or an invalid spread. The client should therefore track:

  • Last applied message identifier
  • Last applied event timestamp
  • Snapshot timestamp
  • Exchange or symbol session
  • Whether the local state is synchronized
  • Whether a reconciliation request is in progress

A state flag such as synchronized or stale is often more useful than a complex boolean combination scattered throughout the application.

Backpressure

REST clients naturally limit load by controlling how many requests they send. WebSocket clients need explicit control when processing is slower than incoming messages.

A robust pipeline may use:

  • A bounded queue between the socket reader and the strategy
  • Backpressure signaling when the queue reaches capacity
  • A reconnect or snapshot strategy when updates cannot be applied safely
  • Metrics for queue depth, dropped messages, and processing latency
  • A rule that prevents the strategy from trading on a stale book

A large unbounded queue can hide a broken consumer while increasing memory usage and delaying the moment when the system recognizes that its state is no longer current.

Cost and Operational Complexity

WebSockets reduce repeated request overhead, but they introduce session-management costs. A production deployment must monitor:

  • Active connections
  • Authentication failures
  • Reconnect frequency
  • Heartbeat latency
  • Message-processing delay
  • Queue depth
  • Snapshot and reconciliation frequency
  • Stale-state incidents

REST is not automatically cheaper. Polling at high frequency can create more server work than a single maintained session. The right comparison is total system cost at the required freshness and reliability level.

7. Deployment Recommendations

The best interface depends on the workload, not merely the market category.

User segment Historical workload Real-time workload Recommended pattern
Individual researcher REST kline and export endpoints REST latest snapshot for infrequent checks REST-first, polling only when needed
Individual quant developer REST history for backtests WebSocket depth or market feed for monitoring REST bootstrap plus WebSocket
Institutional research team REST for bulk historical retrieval and reconciliation WebSocket for shared live state Stateful service with explicit snapshots
Live execution team REST for reference data and controlled recovery WebSocket for low-latency updates WebSocket-primary with REST recovery
Dashboard operator REST for initial page load WebSocket for live UI updates REST snapshot followed by streaming
Batch or scheduled job REST only Not required Rate-limited request scheduler

Recommended Defaults

Start with REST when:

  • The data has a known start and end time.
  • The result can be stored and reused.
  • The application processes data in batches.
  • Recovery can occur by repeating a request.
  • The consumer does not need continuous state.

Use WebSocket when:

  • The consumer needs updates as events occur.
  • Polling would create excessive traffic.
  • The system maintains a live local state.
  • The consumer can tolerate a reconnect and reconciliation procedure.
  • The channel is supported for the selected market and symbol.

A hybrid system is usually the most maintainable option for a new market-data application.

8. Choosing the Right TickDB Endpoint

Endpoint selection should follow the state that the application needs.

Application need Endpoint or channel Interface
Historical candles for a completed period /v1/market/kline REST
Current real-time candle /v1/market/kline/latest REST
Live market updates WebSocket market stream WebSocket
Order book depth, where supported depth channel WebSocket
Symbol discovery and validation Available-symbols endpoint REST

The historical kline endpoint supports more than 10 years of cleaned and aligned US equity OHLCV data, which makes it suitable for cross-cycle backtesting. This does not mean that every asset or endpoint provides full tick-level history. For example, the trades endpoint does not support US equities or A-shares. Hong Kong equity and crypto trade data are supported.

The depth channel also has market-specific boundaries:

Market Supported depth
US equities L1
Hong Kong equities L1–L10
Crypto L1–L10
Forex Not supported
Precious metals Not supported
Indices Not supported

A client should not infer channel support from the existence of a symbol. It should validate the symbol and the requested channel before deploying a live consumer.

9. A Production Checklist

Before shipping a combined REST and WebSocket client, verify the following controls.

REST client

  • Load the API key from TICKDB_API_KEY.
  • Send the key through the X-API-Key request header.
  • Apply a timeout to every HTTP request.
  • Handle error codes 1001, 1002, 2002, and 3001.
  • Read Retry-After for rate-limit responses.
  • Use the historical endpoint for completed periods.
  • Use the latest endpoint for current snapshots.
  • Avoid retrying non-idempotent operations without a safe policy.

WebSocket client

  • Authenticate during the connection handshake.
  • Send periodic heartbeat or ping messages.
  • Reconnect with exponential backoff.
  • Add random jitter to reconnect delays.
  • Validate incoming message structure.
  • Detect sequence gaps when identifiers are available.
  • Refresh state through REST after an uncertain interruption.
  • Bound the internal message queue.
  • Monitor stale state and processing latency.

Application state

  • Bootstrap from a REST snapshot.
  • Track the latest applied event and snapshot.
  • Expose a clear stale or synchronized state.
  • Block trading decisions on stale data where appropriate.
  • Record the source and timestamp of every state transition.

10. The Key Takeaway

REST is the right tool when the application needs an answer. WebSocket is the right tool when the application needs a conversation.

Historical bars, completed periods, latest snapshots, and recovery queries benefit from REST’s bounded request-response model. Live prices, order book updates, and continuous events benefit from WebSocket’s persistent push channel. Using both creates a system that combines the reliability of snapshots with the responsiveness of streaming.

Start with REST for historical context and initial state. Add WebSocket when the strategy or dashboard requires continuous updates. Define the recovery path before the happy path, because a live feed that reconnects without reconciliation is not resilient—it is merely connected.

Next Steps

If you are an individual quant developer, sign up at tickdb.ai to obtain an API key, configure TICKDB_API_KEY, and start with a REST historical query before adding a live stream.

If you are building a production market-data service, combine REST snapshots with WebSocket subscriptions and instrument the snapshot, sequence-gap, reconnect, and stale-state paths.

If you need long historical OHLCV data for backtesting, review the available coverage and request options for individual, professional, or enterprise use.

If you use AI coding assistants, search for and install the tickdb-market-data SKILL in your tool’s marketplace.

This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results.