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:

  1. 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.

  2. 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.

  3. 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 pong response. May contain optional application data (up to 125 bytes).
  • pong: Sent in response to a ping. Must echo back the application data from the ping frame.
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 pong responses (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 pong responses 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

  1. No heartbeat loop: The application code does not send ping messages or run a heartbeat timer. RFC 6455 ping/pong is handled by the websocket-client library at the protocol level.

  2. Connection health monitoring: The _check_connection_health() method serves as a complementary safety net. If no message (including server-initiated pong frames) is received for 90 seconds, the client assumes the connection is dead and reconnects proactively.

  3. Exponential backoff with jitter: Prevents thundering herd when multiple clients reconnect simultaneously after a server outage.

  4. Thread-safe reconnection: The _lock ensures that the stop() 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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:

  1. Always implement reconnection logic: Connections will drop. Plan for it.
  2. Use exponential backoff with jitter: Never reconnect immediately after a failure.
  3. Preserve subscription state: Store your active subscriptions in memory so you can re-subscribe after reconnecting.
  4. Log connection state transitions: You cannot debug what you cannot see.
  5. Set timeouts on all blocking operations: Never wait indefinitely for a network operation.
  6. Monitor connection idle time: Even with native heartbeat, track when you last received data.
  7. 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:

  1. Sign up at tickdb.ai (free tier available, no credit card required)
  2. Generate an API key in the dashboard
  3. Set TICKDB_API_KEY as an environment variable
  4. 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.