The 3 AM Production Alert
At 3:17 AM on a Tuesday, your monitoring dashboard fires an alert. The live trading signal system has gone silent. No new market data for the past 47 seconds — an eternity in quant trading. You scramble to the logs and discover the WebSocket connection dropped silently. No error. No disconnect frame. Just... silence.
When you reconnect, you realize the gap cost you a critical momentum signal. The trade you would have entered at 9:42:13 AM never fired. You are now two basis points behind.
This is not a hypothetical scenario. It is the most common failure mode in production WebSocket systems — the silent connection death. And the difference between a system that survives this and one that craters often comes down to a single feature: the heartbeat mechanism.
What Is a WebSocket Heartbeat?
A WebSocket heartbeat is a periodic signal exchanged between client and server to verify that the connection is still alive. Unlike TCP keepalive — which operates at the network layer and can take minutes to trigger — application-layer heartbeats are configurable, lightweight, and immediately actionable.
In the context of financial data feeds, heartbeat serves three critical functions:
Connection liveness detection: If no data flows for a configurable interval, the client sends a heartbeat. No response within the timeout means the connection is dead, even if TCP has not yet declared it so.
Intermediate proxy timeout prevention: Corporate firewalls, load balancers, and reverse proxies often terminate "idle" TCP connections after 30–120 seconds of inactivity. Periodic heartbeats prevent these intermediaries from pruning the connection.
Server-side resource management: Some servers use heartbeats as a signal to clean up stale connections that have lost their client but not explicitly closed.
The technical foundation for WebSocket heartbeats is defined in RFC 6455, which specifies two control frame types: ping and pong.
RFC 6455 and the ping/pong Protocol
The WebSocket protocol (RFC 6455) defines four control frame types: text, binary, close, ping, and pong. The ping and pong frames are specifically designed for heartbeats:
- ping: Sent by either endpoint to request a
pongresponse. May contain optional application data (up to 125 bytes). - pong: Sent in response to a
ping. Must echo back the application data from thepingframe.
Client Server
| |
|------- ping (opcode 0x9) ----------->|
|<------ pong (opcode 0xA) ------------|
| |
The protocol is intentionally simple. RFC 6455 does not mandate:
- A heartbeat interval (this is application-defined)
- Automatic
pongresponses (some implementations handle this at the library level) - Reconnection logic (this is always the application's responsibility)
This flexibility is a double-edged sword. It allows implementers to tailor heartbeat behavior to their use case, but it also means that native support for heartbeat is a library choice, not a protocol requirement. Many WebSocket servers expose ping capability but do not expose it in their client SDKs, forcing developers to implement heartbeat at the application layer.
The Engineering Trade-off: Native vs. DIY Heartbeat
When evaluating WebSocket APIs for financial data, you will encounter two approaches:
Approach 1: Native heartbeat (TickDB)
The server sends periodic ping frames. The SDK automatically responds with pong frames. If a pong is not received within the expected window, the SDK triggers a reconnection event. The application code receives this as a connection health update or error event.
Advantages:
- Zero application code for the heartbeat loop
- Server-side control ensures both endpoints agree on the interval
- Built-in handling of the RFC 6455 frame format
- Reconnection logic is tested and production-hardened
Disadvantages:
- You are dependent on the server's heartbeat interval settings
- You cannot customize the interval without server-side support
Approach 2: DIY heartbeat (common in other APIs like Polygon)
The application must implement its own heartbeat mechanism using text or binary data frames — typically JSON payloads like {"type": "ping"} — and the server must echo them back as {"type": "pong"}. The application runs a timer, sends the heartbeat frame, and expects a response within a timeout.
Advantages:
- Full control over the interval and format
- Works with any WebSocket server that supports text/binary frames
Disadvantages:
- Application code must manage the timer, the heartbeat dispatch, and the response timeout
- Error handling for missed
pongresponses must be implemented from scratch - The "heartbeat" is semantically different from an RFC 6455
ping— it is a protocol within a protocol - In high-frequency scenarios, the heartbeat loop competes for thread time with data processing
Why Polygon Requires DIY Heartbeat
Polygon.io, a popular US equity data provider, explicitly documents the DIY heartbeat pattern in their WebSocket guide:
"You must send a ping message every 5 seconds. If you do not send a ping within 5 seconds of the last message received, the connection will be closed."
This is a valid implementation strategy, but it places the burden of correctness entirely on the client. Consider the failure modes:
| Failure Mode | DIY Implementation Impact |
|---|---|
| Timer drift under high CPU load | Missed ping → connection dropped by server |
| Thread pool exhaustion | Heartbeat loop stalls → connection dropped by server |
| Network latency spike | ping arrives late → premature timeout trigger |
| SDK bug in timer implementation | Silent failure → connection appears alive but is not |
| Code refactor accidentally removes heartbeat | Production outage → 3 AM alert |
Every one of these failure modes requires custom debugging, custom error handling, and custom retry logic. For a quant team running 50+ strategies across multiple instruments, this is not a trivial engineering tax.
TickDB's Native ping/pong Implementation
TickDB's WebSocket API natively supports RFC 6455 ping/pong frames. The server sends a ping frame every 30 seconds (a conservative interval that survives most proxy timeouts while minimizing overhead). The SDK handles the pong response automatically at the protocol level — no application code required.
If the server does not receive a pong response within the expected window, the connection is terminated and the SDK transitions to reconnection mode.
Production-Grade Python Client with TickDB
The following code demonstrates a complete, production-ready TickDB WebSocket client that leverages native heartbeat. Note that the heartbeat logic is absent by design — it is handled entirely by the SDK. Your application code focuses on data processing and reconnection strategy.
import os
import json
import time
import logging
import random
import threading
from websocket import create_connection, WebSocketTimeoutException, WebSocketConnectionClosedException
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
logger = logging.getLogger("tickdb_websocket")
class TickDBWebSocketClient:
"""
Production-grade TickDB WebSocket client.
Key features:
- Native RFC 6455 ping/pong handling (server-initiated)
- Exponential backoff with jitter on reconnect
- Rate-limit handling (code 3001 + Retry-After header)
- Thread-safe reconnection with daemon flag
- Graceful shutdown
Note: Heartbeat (ping/pong) is handled natively by the underlying
WebSocket implementation. No application-layer heartbeat loop required.
"""
def __init__(
self,
api_key: str = None,
base_url: str = "wss://api.tickdb.ai/ws/market",
max_retries: int = 10,
base_delay: float = 1.0,
max_delay: float = 60.0,
subscription_timeout: float = 5.0
):
self.api_key = api_key or os.environ.get("TICKDB_API_KEY")
if not self.api_key:
raise ValueError(
"API key required. Set TICKDB_API_KEY environment variable "
"or pass api_key parameter."
)
self.base_url = f"{base_url}?api_key={self.api_key}"
self.max_retries = max_retries
self.base_delay = base_delay
self.max_delay = max_delay
self.subscription_timeout = subscription_timeout
self._ws = None
self._running = False
self._retry_count = 0
self._lock = threading.Lock()
# Subscription state
self._subscriptions = set()
self._last_message_time = time.time()
logger.info(f"Initialized TickDB WebSocket client for base URL: {base_url}")
def connect(self) -> bool:
"""
Establish WebSocket connection with retry logic.
Returns True on success, False on permanent failure.
"""
with self._lock:
if self._ws is not None:
try:
self._ws.close()
except Exception:
pass
self._ws = None
try:
# ⚠️ Note: This timeout applies to the connection establishment.
# Heartbeat (ping/pong) is handled internally by the WebSocket library.
self._ws = create_connection(
self.base_url,
timeout=10,
enable_multithread=True
)
self._retry_count = 0
self._last_message_time = time.time()
logger.info("WebSocket connection established")
return True
except Exception as e:
logger.error(f"Connection failed: {e}")
return False
def subscribe(self, symbols: list, channels: list = None):
"""
Subscribe to real-time data for given symbols.
Args:
symbols: List of ticker symbols (e.g., ["NVDA.US", "AAPL.US"])
channels: List of channels (default: ["trades", "depth"])
"""
if channels is None:
channels = ["trades", "depth"]
channels_str = ",".join(channels)
symbols_str = ",".join(symbols)
subscribe_msg = {
"cmd": "subscribe",
"args": {
"channels": channels_str,
"symbols": symbols_str
}
}
try:
self._ws.send(json.dumps(subscribe_msg))
self._subscriptions.update(symbols)
logger.info(f"Subscribed to {symbols} on channels {channels}")
except Exception as e:
logger.error(f"Subscribe failed: {e}")
raise
def _handle_message(self, raw_message: str):
"""
Process incoming WebSocket message.
Note: RFC 6455 ping frames are handled automatically by the
WebSocket library and do NOT appear as application messages.
If you are seeing ping frames in this handler, your library
is misconfigured.
"""
try:
data = json.loads(raw_message)
self._last_message_time = time.time()
# Handle error responses
if "code" in data and data["code"] != 0:
self._handle_error(data)
return
# Dispatch to appropriate handler
msg_type = data.get("type") or data.get("channel")
if msg_type == "depth":
self._process_depth(data)
elif msg_type == "trade" or msg_type == "trades":
self._process_trade(data)
elif msg_type == "pong":
# Application-layer heartbeat response (if server uses this pattern)
logger.debug("Received pong response")
else:
logger.debug(f"Unhandled message type: {msg_type}")
except json.JSONDecodeError:
logger.warning(f"Non-JSON message received: {raw_message[:100]}")
except Exception as e:
logger.error(f"Error processing message: {e}")
def _process_depth(self, data: dict):
"""Process order book depth update."""
symbol = data.get("symbol", "UNKNOWN")
bids = data.get("b", [])
asks = data.get("a", [])
# Calculate buy/sell pressure ratio
bid_volume = sum(float(size) for _, size in bids[:5])
ask_volume = sum(float(size) for _, size in asks[:5])
if ask_volume > 0:
pressure_ratio = bid_volume / ask_volume
logger.info(
f"Depth update | {symbol} | "
f"Bid vol: {bid_volume:.0f} | Ask vol: {ask_volume:.0f} | "
f"Pressure: {pressure_ratio:.2f}"
)
def _process_trade(self, data: dict):
"""Process trade tick."""
symbol = data.get("symbol", "UNKNOWN")
price = data.get("p", 0)
volume = data.get("v", 0)
side = data.get("side", "UNKNOWN")
logger.info(f"Trade | {symbol} | {side} | Price: {price} | Vol: {volume}")
def _handle_error(self, data: dict):
"""Handle error response from server."""
code = data.get("code", 0)
message = data.get("message", "Unknown error")
if code == 3001:
# Rate limited — extract Retry-After
retry_after = int(data.get("headers", {}).get("Retry-After", 5))
logger.warning(f"Rate limited. Retrying after {retry_after} seconds.")
time.sleep(retry_after)
else:
logger.error(f"Server error {code}: {message}")
def _calculate_backoff(self, retry_count: int) -> float:
"""
Calculate delay with exponential backoff and jitter.
Formula: delay = min(base * 2^retry + random(0, base * 0.1), max_delay)
"""
delay = min(self.base_delay * (2 ** retry_count), self.max_delay)
jitter = random.uniform(0, delay * 0.1)
return delay + jitter
def _check_connection_health(self):
"""
Check if connection is still healthy based on last message time.
If no message (including pong) received for > 90 seconds,
the connection is likely dead. Initiate reconnection.
This complements native ping/pong by catching edge cases
where the WebSocket library may not detect a dead connection.
"""
idle_time = time.time() - self._last_message_time
if idle_time > 90:
logger.warning(
f"Connection idle for {idle_time:.0f}s. "
"Likely dead — initiating reconnection."
)
return False
return True
def run(self, symbols: list, channels: list = None):
"""
Main event loop with automatic reconnection.
This loop handles:
- Message receiving with timeout
- Connection health monitoring
- Automatic reconnection on failure
- Graceful shutdown on interrupt
Args:
symbols: List of symbols to subscribe
channels: List of channels to subscribe
"""
self._running = True
self._retry_count = 0
while self._running and self._retry_count < self.max_retries:
if self._ws is None:
if not self.connect():
delay = self._calculate_backoff(self._retry_count)
logger.info(f"Retrying in {delay:.1f} seconds...")
time.sleep(delay)
self._retry_count += 1
continue
# Re-subscribe on new connection
self.subscribe(symbols, channels)
try:
# Receive with timeout allows periodic health checks
# ⚠️ For production HFT workloads, consider asyncio/aiohttp
# for non-blocking I/O and tighter latency control.
message = self._ws.recv()
self._handle_message(message)
except WebSocketTimeoutException:
# Timeout is normal — check connection health periodically
if not self._check_connection_health():
with self._lock:
if self._ws:
self._ws.close()
self._ws = None
continue
except WebSocketConnectionClosedException:
logger.warning("Connection closed by server")
with self._lock:
self._ws = None
self._retry_count += 1
delay = self._calculate_backoff(self._retry_count)
logger.info(f"Reconnecting in {delay:.1f} seconds...")
time.sleep(delay)
except Exception as e:
logger.error(f"Unexpected error: {e}")
with self._lock:
if self._ws:
self._ws.close()
self._ws = None
self._retry_count += 1
if self._retry_count >= self.max_retries:
logger.error(
f"Max retries ({self.max_retries}) exceeded. "
"Manual intervention required."
)
def stop(self):
"""Graceful shutdown."""
logger.info("Shutting down WebSocket client...")
self._running = False
with self._lock:
if self._ws:
try:
self._ws.close()
except Exception:
pass
self._ws = None
if __name__ == "__main__":
# Initialize client
client = TickDBWebSocketClient(
max_retries=10,
base_delay=1.0,
max_delay=60.0
)
# Subscribe to US equity symbols
symbols = ["NVDA.US", "AAPL.US", "TSLA.US"]
try:
client.run(symbols=symbols, channels=["depth", "trades"])
except KeyboardInterrupt:
logger.info("Interrupted by user")
finally:
client.stop()
Key Engineering Notes
No heartbeat loop: The application code does not send
pingmessages or run a heartbeat timer. RFC 6455ping/pongis handled by thewebsocket-clientlibrary at the protocol level.Connection health monitoring: The
_check_connection_health()method serves as a complementary safety net. If no message (including server-initiatedpongframes) is received for 90 seconds, the client assumes the connection is dead and reconnects proactively.Exponential backoff with jitter: Prevents thundering herd when multiple clients reconnect simultaneously after a server outage.
Thread-safe reconnection: The
_lockensures that thestop()method can safely close the connection from another thread.
Comparison: WebSocket Heartbeat Implementation
| Feature | TickDB | Polygon.io | Alpaca | Generic WebSocket SDK |
|---|---|---|---|---|
| Native RFC 6455 ping/pong | ✅ Server-initiated | ❌ DIY (text frame) | ❌ DIY (text frame) | Depends on SDK |
| Configurable heartbeat interval | ❌ Fixed (server-side) | ✅ Client-controlled | ✅ Client-controlled | Depends on SDK |
| Automatic pong response | ✅ SDK handles | ❌ Application handles | ❌ Application handles | Depends on SDK |
| Application code for heartbeat | ❌ Not required | ✅ Required (5s mandatory) | ✅ Required | Varies |
| Connection health callback | ✅ Via timeout | ✅ Via heartbeat timeout | ✅ Via heartbeat timeout | Varies |
| Built-in reconnection | ✅ On disconnect | ❌ DIY | ❌ DIY | Varies |
The key distinction: TickDB's native heartbeat shifts the implementation burden from the application to the SDK. For teams running multiple strategies, this means:
- Less custom code: No heartbeat timer, no response parser, no timeout handler.
- Fewer failure modes: No timer drift, no missed heartbeats due to CPU contention.
- Faster onboarding: A developer unfamiliar with WebSocket internals can connect successfully.
Why This Matters for Quant Systems
Quant trading systems have unique reliability requirements that amplify the importance of heartbeat architecture:
Multi-strategy portfolios: A team running 20 strategies on 50 symbols cannot afford to debug heartbeat failures across every strategy independently. Centralized, native heartbeat is operationally simpler.
Overnight positions: Strategies holding overnight exposure depend on after-hours data feeds staying alive through the 16+ hour market close. Proxy timeout prevention is not optional.
Event-driven signals: Post-earnings volatility spikes can last 30–120 seconds. A dropped connection at the wrong moment means a missed signal and a missed trade.
Backtest-live parity: When you can point to the same WebSocket infrastructure for both historical data (via REST) and live data (via WebSocket), you reduce the "my backtest worked but live trading failed" problem.
Best Practices for WebSocket Connection Management
Regardless of whether your provider offers native heartbeat, follow these engineering practices:
- Always implement reconnection logic: Connections will drop. Plan for it.
- Use exponential backoff with jitter: Never reconnect immediately after a failure.
- Preserve subscription state: Store your active subscriptions in memory so you can re-subscribe after reconnecting.
- Log connection state transitions: You cannot debug what you cannot see.
- Set timeouts on all blocking operations: Never wait indefinitely for a network operation.
- Monitor connection idle time: Even with native heartbeat, track when you last received data.
- Test failure scenarios explicitly: Kill the network, kill the server, and verify your client recovers gracefully.
Next Steps
If you're evaluating WebSocket data providers, the heartbeat implementation is a reliable indicator of engineering maturity. Ask your vendor: "How does the client handle connection liveness?" If the answer involves "you need to send a ping every X seconds," you are accepting technical debt.
If you want to explore TickDB's native heartbeat with a real data stream:
- Sign up at tickdb.ai (free tier available, no credit card required)
- Generate an API key in the dashboard
- Set
TICKDB_API_KEYas an environment variable - Clone the client code above and run it against your symbols of interest
If you're building a multi-strategy system and need unified market data across US equities, HK equities, and crypto:
- US equity historical OHLCV: 10+ years of cleaned, aligned data via
/v1/market/kline - Real-time depth and trades: Native WebSocket with RFC 6455 heartbeat
- Cross-asset correlation: Single API, single SDK, unified symbol format
If you use AI coding assistants, search for and install the tickdb-market-data SKILL in your AI tool's marketplace for integrated TickDB API access directly from your development environment.
This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results.