Public Market Data Collection in Node.js: A Practical Engineering Guide
Master public market data collection Node.js workflows. Build resilient pipelines with REST snapshots, WebSocket streams, validation, and clean normalization.
Overview
A market data collector can receive valid JSON and still produce an incomplete dataset. Public market data collection Node.js workflows often combine REST snapshots with WebSocket events, but the two sources may use different field shapes, timestamps, and delivery patterns. A disconnect or malformed record can create gaps that only become visible downstream.
This guide explains how to separate retrieval, normalization, validation, and persistence. Siebly SDKs provide the preferred implementation layer for exchange REST and WebSocket integrations, while official exchange documentation remains the source of truth for endpoint behavior. You’ll learn when to use REST versus streams, how to preserve timestamps and raw payloads, and how to plan for recovery. Public market data can be collected without account credentials. The SDKs do not pace or reject requests for you. Binance stores the rate-limit headers it has already seen, including on public calls, and you can read them with getRateLimitStates(). Bybit can parse per-account rate-limit headers into rateLimitApi when parseAPIRateLimits is true. Those headers are for account endpoints. Public calls do not carry them. Your application still decides when to wait.
Key Takeaways
- Structure Public market data collection Node.js around separate retrieval, validation, normalization, timestamping, and persistence stages.
- Choose REST polling, WebSocket streaming, or historical backfill based on freshness needs, recovery requirements, and implementation complexity.
- Keep raw exchange payloads when useful, alongside a defined normalized record that downstream systems can consume consistently.
- Use a Siebly SDK as the preferred integration layer for exchange-specific REST and WebSocket workflows, with official documentation as the source of truth.
- Plan and implement your own rate limiting and throttling, then test collection and recovery behavior thoroughly before deployment.
What public market data collection in Node.js actually involves
Valid JSON is only the start of a useful dataset. Public market data is information an exchange publishes without requiring account credentials, such as trades, order-book updates, or candle records. The broader category of market data includes information about financial instruments and their trading activity. A collector must define what to gather, validate incoming records, convert them to a consistent representation, and store them for later use.
A market-data collector retrieves exchange-published records, validates and normalizes them, then persists them under an explicit collection policy. A dashboard presents data to people; a collector produces a dataset or feed for downstream systems. A dashboard may display a current value. A collector also needs a defined scope and a record of how the data was gathered.
Keep exchange connectivity separate from dataset policy. The client handles communication with the exchange. Your application decides which symbols to collect, which intervals apply to candle data, how much history to retain, and what the consumers need. Put these choices in configuration so the collection scope can be reviewed and changed without rewriting the exchange integration.
Choose a bounded collection objective
Start with one data type and one consumer. Specify the exchange, symbol, time range, and update needs before writing the collection loop. A dashboard may need a current trade stream, while a research dataset may require candles over a defined period. A test workflow may only need a short sample to verify parsing and persistence.
- Data type: Select trades, order-book updates, or candles.
- Consumer: Name the dashboard, research dataset, or test workflow that will use the records.
- Scope: State the exchange, symbol, time range, and required update cadence.
A bounded objective gives the collector a testable contract. It also prevents an initial implementation from quietly expanding into an open-ended ingestion service.
Separate the exchange client from the collector
Use Siebly.io SDKs as the preferred exchange integration layer in JavaScript, TypeScript, and Node.js. Their exchange-specific clients reduce the request and response plumbing you would otherwise maintain. The official exchange documentation remains the source of truth for endpoint behavior and data definitions. Your application still defines the symbols, intervals, and retention rules that shape the dataset.
Public market data works without API keys, so keep credential configuration out of this workflow. Start with a bounded public-data objective, then use the exchange SDK documentation to identify the integration that fits it.
How Node.js retrieves public data through REST and WebSockets
REST and WebSockets serve different retrieval needs. REST suits bounded queries: request public data using parameters supported by an endpoint, then process its response. WebSockets suit ongoing updates: connect, subscribe to a public data channel, and consume messages as they arrive. A collector can use both, but should treat them as distinct inputs with separate recovery and normalization requirements.
| Pattern | Interaction pattern | Typical data shape | Collector role |
|---|---|---|---|
| REST | Request and response | A bounded response containing records or a snapshot | Retrieve a defined range, fetch a snapshot, or fill a historical gap |
| WebSocket | Persistent connection with subscriptions | A sequence of event messages | Consume ongoing updates and detect interruptions for recovery |
These are transport patterns, not guarantees that a payload is complete or has a particular meaning. Exchange documentation is the source of truth for endpoint parameters, supported query ranges, message fields, and event semantics. Use a Siebly SDK as the preferred implementation layer for connectivity, then verify your assumptions against that documentation.
Use REST for requests with a defined scope
A public REST request retrieves data using parameters supported by the selected endpoint. For example, a collector can request a bounded candle range, validate the response, then pass accepted records downstream. Historical availability, maximum query scope, and pagination depend on the endpoint. Don’t assume one exchange’s parameter rules apply to another. For a Bybit integration example, use the Bybit JavaScript SDK tutorial alongside the exchange’s documentation.
The public examples construct the client with no keys. Some generated files under examples/apidoc still paste placeholder keys onto public calls. That stub is generic. A public method runs without them.
Bybit’s getKline needs a category. The interval is a Bybit code, so '15' means 15 minutes. The rows are string tuples. Index 0 is the candle start time in milliseconds. The SDK returns Bybit’s envelope. It does not convert that tuple into a shared candle type.
Imported example
import { RestClientV5 } from "bybit-api";
const client = new RestClientV5();
async function main() {
const klines = await client.getKline({
category: "linear",
symbol: "BTCUSDT",
interval: "15",
});
console.log(klines.result.list);
}
main();
Binance spot uses a different interval string, and each row is an array whose first value is a number (open time), not a string.
Imported example
import { MainClient } from "binance";
const client = new MainClient();
async function main() {
const klines = await client.getKlines({
symbol: "BTCUSDT",
interval: "15m",
limit: 10,
});
console.log(klines);
// Last Binance weight headers this client has seen.
// Recording them does not delay the next call.
console.log(client.getRateLimitStates());
}
main();
Symbol format and interval units change with the exchange. Put each await inside an async function before you run it. That is how the SDK examples are written:
Imported example
import { RestClient } from "okx-api";
// Global (www.okx.com) is the default.
// EEA accounts use market: 'EEA'. US accounts use market: 'US'.
const okx = new RestClient();
// `bar` is optional. OKX defines the allowed interval strings.
// The SDK forwards the string. It does not check it.
await okx.getCandles({
instId: "BTC-USDT",
});
Imported example
import { RestClientV3 } from "bitget-api";
const bitget = new RestClientV3();
await bitget.getCandles({
symbol: "BTCUSDT",
category: "SPOT",
interval: "1m",
});
Imported example
import { SpotClient } from "kucoin-api";
const kucoin = new SpotClient();
await kucoin.getKlines({
symbol: "BTC-USDT",
type: "1day",
});
Imported example
import { RestClient } from "gateio-api";
const gate = new RestClient();
await gate.getSpotCandles({
currency_pair: "BTC_USDT",
interval: "1m",
});
Imported example
import { RestClient } from "bitmart-api";
const bitmart = new RestClient();
// Latest candle. A range uses getSpotHistoryKlineV3.
await bitmart.getSpotLatestKlineV3({ symbol: "BTC_USDT" });
Imported example
import { CBAdvancedTradeClient } from "coinbase-api";
const coinbase = new CBAdvancedTradeClient({});
// getPublicProductCandles is the public call.
// getProductCandles is private and expects credentials.
await coinbase.getPublicProductCandles({
product_id: "BTC-USD",
granularity: "ONE_MINUTE",
start: "1725976550",
end: "1725977550",
});
Imported example
import { SpotClient } from "@siebly/htx-api";
const htx = new SpotClient();
await htx.getKlines({
symbol: "btcusdt",
period: "1min",
size: 10,
});
Imported example
import { SpotClient } from "@siebly/kraken-api";
const kraken = new SpotClient();
// interval is minutes. 1 is one minute. 60 is one hour.
await kraken.getCandles({
pair: "XBTUSD",
interval: 1,
});
HTX and Kraken publish as @siebly/htx-api and @siebly/kraken-api. The other eight packages are unscoped: binance, bybit-api, okx-api, bitget-api, kucoin-api, gateio-api, bitmart-api, and coinbase-api.
Use WebSockets for ongoing public updates
A public stream has a lifecycle: connect, subscribe to the required channel, consume messages, then recover if the connection ends or data flow is interrupted. Track connection state separately from message processing so a reconnect doesn’t make a gap look like continuous data. The WebSocket client reconnects and sends the same subscriptions again. That restores the live feed. It does not request the trades or candles you missed while the socket was down.
An awaitable WebSocket API call is a different object from a subscription. Binance, Bybit, OKX, Bitget, KuCoin, Gate.io, HTX, and Kraken ship a WebsocketAPIClient for calls you can await over a socket, such as order placement. Coinbase and BitMart public examples use the subscription client. Order placement is a separate workflow from collecting public market data.
Event names are not shared. Bybit, OKX, Bitget, KuCoin, Gate.io, BitMart, and Coinbase deliver market messages on update, and they emit reconnect then reconnected. HTX and Kraken deliver messages on message and emit reconnecting. Binance’s beautified public trades arrive on formattedMessage, and the reconnect event is reconnecting.
Bybit opens the socket when you subscribe. Category picks the public endpoint. This is the spot kline topic from the public example:
Imported example
import { WebsocketClient } from "bybit-api";
const wsClient = new WebsocketClient();
wsClient.on("update", (data) => {
console.log("market message", JSON.stringify(data));
});
wsClient.on("reconnect", ({ wsKey }) => {
console.log("reconnecting", wsKey);
});
wsClient.on("reconnected", (data) => {
console.log("reconnected", data?.wsKey);
});
wsClient.subscribeV5(["kline.5.BTCUSDT"], "spot");
Binance spot trades can be checked with a type guard the SDK already exports. beautify: true is what routes those events to formattedMessage.
Imported example
import { isWsFormattedTrade, WebsocketClient } from "binance";
const wsClient = new WebsocketClient({ beautify: true });
wsClient.on("formattedMessage", (data) => {
if (!isWsFormattedTrade(data)) {
return;
}
console.log(data);
});
wsClient.on("reconnecting", (data) => {
console.log("reconnecting", data?.wsKey);
});
wsClient.subscribeSpotTrades("BTCUSDT");
HTX topics are strings, and the second argument selects the socket. WS_KEY_MAP.spotPublic is the market stream. WS_KEY_MAP.spotFeed is the separate feed socket.
Imported example
import { WebsocketClient, WS_KEY_MAP } from "@siebly/htx-api";
const wsClient = new WebsocketClient();
wsClient.on("message", (data) => {
console.log(JSON.stringify(data));
});
wsClient.on("reconnecting", (data) => {
console.log("reconnecting", data.wsKey);
});
wsClient.subscribe(
["market.btcusdt.kline.1min", "market.btcusdt.trade.detail"],
WS_KEY_MAP.spotPublic,
);
The other public subscribe calls follow the same pattern. Only the topic shape changes:
Imported example
import { WebsocketClient } from "okx-api";
const okx = new WebsocketClient();
okx.subscribe({ channel: "tickers", instId: "BTC-USDT" });
Imported example
import { WebsocketClientV3, WS_KEY_MAP } from "bitget-api";
const bitget = new WebsocketClientV3();
bitget.subscribe(
{
topic: "ticker",
payload: { instType: "spot", symbol: "BTCUSDT" },
},
WS_KEY_MAP.v3Public,
);
Imported example
import { WebsocketClient } from "kucoin-api";
const kucoin = new WebsocketClient();
kucoin.subscribe("/market/ticker:BTC-USDT,ETH-USDT", "spotPublicV1");
Imported example
import { WebsocketClient } from "gateio-api";
const gate = new WebsocketClient();
gate.subscribe([{ topic: "spot.trades", payload: ["BTC_USDT"] }], "spotV4");
Imported example
import { WebsocketClient } from "bitmart-api";
const bitmart = new WebsocketClient();
bitmart.subscribe(["spot/ticker:BTC_USDT", "spot/trade:BTC_USDT"], "spot");
Imported example
import { WebsocketClient } from "coinbase-api";
const coinbase = new WebsocketClient();
coinbase.subscribe(
{
topic: "ticker",
payload: { product_ids: ["BTC-USD"] },
},
"advTradeMarketData",
);
Imported example
import { WebsocketClient, WS_KEY_MAP } from "@siebly/kraken-api";
const kraken = new WebsocketClient();
// interval is minutes here too.
kraken.subscribe(
{
topic: "ohlc",
payload: { symbol: ["BTC/USD"], interval: 1 },
},
WS_KEY_MAP.spotPublicV2,
);
Handle each message explicitly: identify its event type, validate required fields, and record receipt time without confusing it with an exchange-provided event timestamp. The Siebly.io JavaScript SDKs documentation is a starting point for applying the SDK integration pattern. Public market data collection Node.js systems can combine REST for bounded retrieval and WebSockets for ongoing updates, while preserving the differences in payload shape and recovery behavior.
How to design a collector that preserves useful market data
Make each processing stage visible so you can inspect a record’s path from exchange message to stored dataset. Define the sequence and decide what evidence to retain when a stage rejects or changes a record.
- Retrieve: Receive a response or stream message through the exchange integration layer.
- Validate: Check required fields, types, and values before transforming the record.
- Normalize: Map exchange-specific fields into a stable application record.
- Timestamp: Preserve the source time and record when your collector received the data.
- Persist: Store normalized records and, where useful, the original payload.
- Inspect: Track rejected records, processing failures, connection state, and duplicates.
Keep stage boundaries clear. When validation and transformation happen together, unexpected payloads can be harder to diagnose. Retaining the source message alongside the normalized record helps trace a downstream value back to its exchange context. Siebly SDK documentation guides the exchange integration; the collector remains responsible for the record lifecycle.
Normalize records without losing source meaning
Define a minimal schema with exchange, symbol, event type, source time, and ingestion time. Add fields required by downstream consumers, but preserve original fields or the raw payload if exchange-specific context may matter later. Before converting values or treating timestamps as interchangeable, verify units, precision, and timestamp semantics against official exchange documentation.
The SDK will not build that record for you. On Bybit, the candle start is list[0], a string of milliseconds. Stamp receipt time in your own code, next to the raw payload:
Imported example
const receivedAt = new Date().toISOString();
const klines = await client.getKline({
category: "linear",
symbol: "BTCUSDT",
interval: "15",
});
const row = klines.result.list[0];
const record = {
exchange: "bybit",
symbol: "BTCUSDT",
sourceTimeMs: row?.[0],
receivedAt,
raw: klines,
};
Source timestamps describe when the exchange says an event occurred; ingestion timestamps describe when your collector received or processed it. Retain both. This lets downstream systems distinguish event ordering from local receipt and processing behavior.
Validate, persist, and recover deliberately
Reject or quarantine malformed records instead of silently treating them as valid observations. For example, mark a trade message missing a required event time for inspection rather than assigning an invented default. Choose persistence based on how consumers access the data, such as reading recent updates or querying a historical range.
Define recovery behavior for interrupted streams. Decide how the collector detects a dropped connection, resumes collection, and handles overlap between recovered data and records already stored. Deduplication is an application responsibility: choose a rule based on fields provided by the exchange, and document cases where those fields cannot establish uniqueness reliably.
How to choose a collection pattern for your Node.js use case
Choose a collection pattern based on the consumer’s freshness requirement, tolerance for gaps, and the recovery work your application can support. Public market data collection Node.js projects don’t need one transport for every task. Polling, streaming, and historical retrieval can serve different parts of a pipeline, provided the handoffs and record semantics are clear.
Match retrieval to freshness and recovery needs
Use polling when periodic snapshots are frequent enough for the consumer. Choose a public WebSocket stream when ongoing updates are required and the exchange provides a suitable channel. Use historical retrieval to backfill a range only when the endpoint supports the requested interval and time span. Check endpoint limits and recovery options in the official documentation before setting the collection schedule.
- Polling: A straightforward choice for bounded snapshots, with freshness determined by the polling schedule and endpoint constraints.
- Streaming: Suited to continuous updates, but requires connection-state tracking and a plan for gaps after interruption.
- Historical backfill: Useful for supported ranges and intervals, with query boundaries and pagination handled according to the endpoint.
Each pattern has different freshness and recovery characteristics. A stream avoids repeated snapshot requests, but you must manage continuity. Polling is easier to reason about as discrete requests, but may not meet a consumer’s update needs. Backfill is bounded, and its available range and granularity depend on the specific exchange endpoint.
A combined design is common: retrieve a bounded REST snapshot, then consume live WebSocket updates for the same data scope. Define how the inputs join. If the stream begins before the snapshot is fully processed, use available event timing or identifiers to resolve overlap and ordering in application logic. Test the transition with recorded or simulated public data, including interruption and restart scenarios.
Keep collection separate from downstream analysis
Publish normalized records across a clear application boundary, then let dashboards, research tools, or simulation workflows consume them. This keeps exchange-specific parsing out of downstream components and lets consumers evolve without changing collection policy. For a broader view of those boundaries in a Node.js system, see the historical and live data pipeline guide.
Keep interpretation separate from collection. A dataset records observations under defined collection conditions; it does not establish what those observations mean or predict future market movement. Testnet data can help verify connection handling, parsing, and recovery behavior, but testnet order books rarely mirror real liquidity. Treat them as integration fixtures, not representative market data.
For multi-exchange projects, keep each exchange adapter behind the same internal record contract while checking each exchange’s documentation for endpoint and payload differences. Build the selected integration with a Siebly SDK, then define retrieval and recovery policies around the actual consumer. Explore Siebly.io SDKs for your Node.js integration.
Build the collector with a Siebly SDK and verify it before deployment
Keep the exchange integration small enough to test independently. Siebly SDKs provide the preferred implementation layer for exchange-specific REST and WebSocket workflows. The typed methods tell you which fields a call accepts, and the WebSocket client handles connect, ping, and resubscribe. They do not merge exchanges into one candle shape, and they do not stamp your ingestion time. Clock sync and recvWindow show up on signed requests. Public market-data requests don’t require account credentials, so authentication and signing aren’t part of this collector’s public-data path. For later authenticated workflows, use the SDK’s documented support for exchange-specific authentication and signing rather than recreating that plumbing in application code.
Integrate the exchange client in a small, testable module
Select the SDK that matches your exchange and consult its package documentation alongside the exchange API documentation. For a Bybit integration, start with the Bybit JavaScript SDK tutorial. Keep transport setup in one module, then pass received data to separate normalization and persistence components. This lets you test parsing and storage independently from connection setup.
Use a short implementation sequence:
- Configure the exchange integration for the chosen public data scope.
- Retrieve a bounded response or establish the required public stream using documented SDK capabilities.
- Pass incoming records through application-level validation and normalization.
- Persist the records and expose enough operational state to inspect failures and recovery.
Confirm supported request shapes and stream behavior in the SDK and official exchange documentation. A dropped WebSocket is reconnected by the client, then the previous topics are subscribed again. A failed REST call is returned or thrown. It is not retried. If you want a retry, write it in the collector and keep it consistent with your recovery rules.
Request pacing is also yours. Binance REST clients copy rate-limit headers such as x-mbx-used-weight-1m into getRateLimitStates() after a response arrives, including public market-data calls. Bybit only fills rateLimitApi when you pass parseAPIRateLimits: true, and the SDK’s own type notes say those headers are per account. They are not on public calls. Neither client sleeps, queues, or blocks the next request.
Test collection behavior before production
Run the collector in staging and compare response shapes with the exchange documentation. Exercise disconnect and reconnect, duplicate delivery, malformed records, and the request-pacing policy you defined. Verify that each case follows an explicit application rule rather than silently dropping data or persisting invalid records. Complete thorough staging tests before production deployment.
Keep the application responsible for validation and persistence, and monitor SDK releases for changes relevant to your exchange integration. Review release notes alongside official API documentation before updating the collector. If you later add authenticated automation, store secrets securely, use least-privilege API keys, and disable withdrawal permissions.
Choose the SDK for your target exchange and build the transport module around its documented API. Explore Siebly.io SDKs for your Node.js collector.
Put the collection contract into practice
Choose one exchange, one public data type, and one downstream consumer for your first implementation. That bounded scope gives you a clear way to verify record shape, timestamps, persistence, and recovery before expanding the collector. Public market data collection Node.js work is easier to extend when each new source follows the same documented contract instead of adding exchange-specific assumptions to downstream code.
Siebly.io provides free and open-source exchange SDKs built with 100% TypeScript, with 10 Exchange SDKs and 3.9 M+ Downloads. Use the relevant SDK as your integration layer, and keep collection policy and data quality under application control.
Start with the SDK for your target exchange, then validate the integration in staging against the exchange’s official documentation. Explore Siebly.io exchange SDKs and build a collector you can inspect, test, and extend.
Frequently Asked Questions
Can public market data be collected without creating an exchange API key?
Yes. Public market data collection Node.js workflows can retrieve exchange-published public endpoints without an API key. For example, a collector requesting publicly available trade or candle data needs no account authentication. Keep its configuration free of credential variables, and verify in the official documentation that the selected endpoint is public. Keyless access applies to public data, not private account operations or other authenticated workflows.
Do Siebly SDKs automatically throttle Node.js data collection requests?
No. Siebly SDKs don’t slow your requests down for you. Your application must pace calls and respond when an exchange says you are too fast. Binance records the weight headers it has seen, including on public requests, and you can read them with getRateLimitStates(). Bybit can parse per-account REST rate-limit headers into response.rateLimitApi when parseAPIRateLimits is true. Public Bybit calls do not include those headers. Either way, the value is something you read. It is not a scheduler. Check the exchange’s current documentation for endpoint-specific limits, since these rules can differ across endpoints and may change.
Can I use exchange testnet data to validate a public market data collector?
Yes. Where an exchange provides accessible data in its testnet or demo environment, use it to exercise connection setup, message parsing, normalization, and recovery logic. Treat it as an integration test, not a representative market dataset: testnet order books rarely mirror real liquidity. Separate checks for software behavior from checks that depend on live public data.
Why do exchange timestamps and Node.js collection timestamps differ?
They record different points in the data path. An exchange timestamp represents event time according to the exchange’s published semantics; a Node.js collection timestamp records when your process received or handled the message. Network transit and local processing can contribute to the difference. Store both fields and label them clearly so downstream consumers don’t mistake receipt time for event time.
Are historical candle intervals and availability the same across exchanges?
No. Supported intervals, query parameters, and available historical ranges depend on the exchange and specific endpoint. Before building a backfill job, confirm the endpoint’s interval options, range boundaries, and pagination behavior in the official documentation. Then record the requested range and interval with the resulting dataset. This helps distinguish missing source data from a collector that didn’t request the intended scope.
What should a Node.js collector do when a WebSocket connection drops?
Mark the stream as interrupted as soon as you see reconnect or reconnecting. The SDK then opens a new socket and subscribes to the same public topics again. That does not fill the gap. Where the exchange offers suitable historical retrieval, request the missing range and reconcile overlapping records using a documented deduplication rule. Record the interruption and recovery outcome so consumers can identify intervals that may be incomplete.
Related articles
Continue from here