WebSocket API Documentation
Overviewβ
The Stock Screening Platform provides a WebSocket API for real-time stock price updates, order book data, and market alerts. The WebSocket server supports:
- Real-time updates: Sub-100ms latency for price and order book changes
- Multi-instance support: Redis Pub/Sub for horizontal scalability
- High performance: Message batching, compression, and rate limiting
- Session restoration: Automatic reconnection with subscription recovery
- 10K+ concurrent connections: Load tested and production-ready
Connectionβ
Endpointβ
ws://localhost:8000/v1/ws
wss://api.screener.kr/v1/ws (production)
Authenticationβ
Optional JWT token can be passed as query parameter:
const ws = new WebSocket('ws://localhost:8000/v1/ws?token=YOUR_JWT_TOKEN');
Anonymous connections are supported for public data.
Connection Optionsβ
// Enable compression (recommended for production)
const ws = new WebSocket('ws://localhost:8000/v1/ws', {
perMessageDeflate: true
});
Message Formatβ
All messages are JSON with the following base structure:
{
"type": "message_type",
"timestamp": "2025-11-11T00:00:00Z",
"sequence": 12345,
...additional fields
}
Message Typesβ
Client β Serverβ
subscribe: Subscribe to stock/market updatesunsubscribe: Unsubscribe from updatesping: Heartbeat checkrefresh_token: Refresh JWT tokenreconnect: Reconnect with session restoration
Server β Clientβ
price_update: Stock price changeorderbook_update: Order book (νΈκ°) changemarket_status: Market open/close eventsalert: Price alertserror: Error messagespong: Heartbeat responsebatch: Batched messages (Phase 4)
Subscriptionβ
Subscribe to Stock Updatesβ
{
"type": "subscribe",
"subscription_type": "stock",
"targets": ["005930", "000660", "035720"]
}
Response:
{
"type": "subscribed",
"subscription_type": "stock",
"targets": ["005930", "000660", "035720"],
"timestamp": "2025-11-11T00:00:00Z"
}
Subscribe to Market Updatesβ
{
"type": "subscribe",
"subscription_type": "market",
"targets": ["KOSPI", "KOSDAQ"]
}
Subscribe to Sector Updatesβ
{
"type": "subscribe",
"subscription_type": "sector",
"targets": ["IT", "FINANCE"]
}
Unsubscribeβ
{
"type": "unsubscribe",
"subscription_type": "stock",
"targets": ["005930"]
}
Real-time Updatesβ
Price Updateβ
{
"type": "price_update",
"code": "005930",
"name": "Samsung Electronics",
"price": 73000,
"change": 1000,
"change_percent": 1.39,
"volume": 12345678,
"timestamp": "2025-11-11T09:30:00Z",
"sequence": 12345
}
Order Book Updateβ
{
"type": "orderbook_update",
"code": "005930",
"bids": [
{"price": 72900, "quantity": 1000},
{"price": 72800, "quantity": 1500}
],
"asks": [
{"price": 73000, "quantity": 800},
{"price": 73100, "quantity": 1200}
],
"timestamp": "2025-11-11T09:30:00Z",
"sequence": 12346
}
Market Statusβ
{
"type": "market_status",
"market": "KOSPI",
"status": "open",
"timestamp": "2025-11-11T09:00:00Z",
"sequence": 12347
}
Alertβ
{
"type": "alert",
"code": "005930",
"alert_type": "price_above",
"threshold": 75000,
"current_price": 75100,
"message": "Samsung Electronics exceeded 75,000 KRW",
"timestamp": "2025-11-11T10:00:00Z",
"sequence": 12348
}
Phase 4: Performance Featuresβ
Message Batchingβ
Message batching is enabled by default to reduce network overhead and improve throughput.
Messages are queued and sent in batches every 30ms (configurable):
{
"type": "batch",
"batch_size": 5,
"messages": [
{
"type": "price_update",
"code": "005930",
"price": 73000,
...
},
{
"type": "price_update",
"code": "000660",
"price": 145000,
...
},
...
],
"timestamp": "2025-11-11T09:30:00.030Z",
"sequence": 12349
}
Client Implementation:
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'batch') {
// Process batched messages
message.messages.forEach(msg => {
handleMessage(msg);
});
} else {
// Process single message
handleMessage(message);
}
};
Performance Impact:
- Reduces WebSocket send() calls by ~80%
- Lower CPU usage on both server and client
- Better throughput for high-frequency updates
Rate Limitingβ
100 messages per second per connection (default, configurable)
If rate limit is exceeded:
{
"type": "error",
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded (100 messages/second)",
"details": {
"rate_limit": 100
},
"timestamp": "2025-11-11T09:30:00Z"
}
Best Practices:
- Subscribe only to stocks you're actively monitoring
- Unsubscribe when switching views
- Use market/sector subscriptions for overview pages
See Rate Limiting Guide for more details.
Compressionβ
Per-message deflate compression is auto-negotiated with clients.
Enable in browser:
const ws = new WebSocket('ws://localhost:8000/v1/ws', {
perMessageDeflate: {
clientMaxWindowBits: 14,
serverMaxWindowBits: 14
}
});
Performance Impact:
- Reduces bandwidth by 60-80% for text messages
- Minimal CPU overhead
- Recommended for production
Session Restoration (Phase 3)β
If disconnected, reconnect with your previous session ID:
// Save session ID on connect
let sessionId = null;
ws.onopen = () => {
// Extract connection_id from first message
};
ws.onclose = () => {
// Reconnect with session restoration
const reconnectUrl = `ws://localhost:8000/v1/ws?session_id=${sessionId}&token=${jwt}`;
ws = new WebSocket(reconnectUrl);
};
Session restoration response:
{
"type": "reconnected",
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"restored_subscriptions": {
"stock": ["005930", "000660"],
"market": ["KOSPI"]
},
"missed_messages_count": 5,
"timestamp": "2025-11-11T09:30:10Z"
}
Session TTL: 5 minutes
Error Handlingβ
Error Codesβ
INVALID_MESSAGE: Malformed JSON or invalid message typeINVALID_TOKEN: JWT token validation failedSUBSCRIPTION_FAILED: Unable to subscribe to targetRATE_LIMIT_EXCEEDED: Too many messages sentSESSION_RESTORATION_FAILED: Session not found or expiredTOKEN_REFRESH_ERROR: Token refresh failed
Error Message Formatβ
{
"type": "error",
"code": "ERROR_CODE",
"message": "Human-readable error description",
"details": {
"additional": "context"
},
"timestamp": "2025-11-11T09:30:00Z"
}
Monitoringβ
Connection Statisticsβ
GET /v1/ws/stats
{
"active_connections": 1234,
"total_subscriptions": 5678,
"subscriptions_by_type": {
"stock": 4000,
"market": 1000,
"sector": 678
},
"messages_sent": 123456,
"saved_sessions": 5,
"batching_enabled": true,
"batch_interval_ms": 30,
"queued_messages": 45,
"rate_limiting_enabled": true,
"rate_limit": 100
}
Connection Listβ
GET /v1/ws/connections
[
{
"connection_id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "user-123",
"connected_at": "2025-11-11T09:00:00Z",
"subscriptions": {
"stock": ["005930", "000660"],
"market": ["KOSPI"]
},
"message_count": 1234,
"last_activity": "2025-11-11T09:30:00Z"
}
]
Load Testingβ
Use the provided load testing script:
cd backend/tests/load
python websocket_load_test.py --connections 10000 --duration 60
Options:
--url: WebSocket URL (default: ws://localhost:8000/v1/ws)--connections: Number of concurrent connections (default: 1000)--duration: Test duration in seconds (default: 60)--ramp-up: Ramp-up time in seconds (default: 10)--batch-size: Connections per batch (default: 100)
Performance Targetsβ
| Metric | Target | Achieved |
|---|---|---|
| Concurrent connections | 10,000 | β |
| Latency (p99) | < 100ms | β |
| Message throughput | 100,000 msg/s | β |
| Memory per connection | < 100KB | β |
| CPU usage (10K connections) | < 50% | β |
Best Practicesβ
Client Implementationβ
class StockWebSocket {
constructor(url, token) {
this.url = url;
this.token = token;
this.ws = null;
this.sessionId = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
}
connect() {
const wsUrl = this.sessionId
? `${this.url}?session_id=${this.sessionId}&token=${this.token}`
: `${this.url}?token=${this.token}`;
this.ws = new WebSocket(wsUrl, {
perMessageDeflate: true // Enable compression
});
this.ws.onopen = () => {
console.log('Connected');
this.reconnectAttempts = 0;
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleMessage(message);
};
this.ws.onclose = () => {
console.log('Disconnected');
this.reconnect();
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
handleMessage(message) {
// Handle batch messages
if (message.type === 'batch') {
message.messages.forEach(msg => this.processMessage(msg));
return;
}
this.processMessage(message);
}
processMessage(message) {
switch (message.type) {
case 'price_update':
this.onPriceUpdate(message);
break;
case 'orderbook_update':
this.onOrderBookUpdate(message);
break;
case 'error':
this.onError(message);
break;
// ... handle other types
}
}
subscribe(type, targets) {
this.ws.send(JSON.stringify({
type: 'subscribe',
subscription_type: type,
targets: targets
}));
}
reconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnect attempts reached');
return;
}
this.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(() => this.connect(), delay);
}
}
Subscription Managementβ
// Subscribe to individual stocks
ws.subscribe('stock', ['005930', '000660', '035720']);
// Subscribe to markets for overview
ws.subscribe('market', ['KOSPI', 'KOSDAQ']);
// Unsubscribe when leaving detail page
ws.unsubscribe('stock', ['005930']);
Memory Managementβ
// Limit subscription count to prevent rate limiting
const MAX_SUBSCRIPTIONS = 50;
function addSubscription(code) {
if (subscriptions.size >= MAX_SUBSCRIPTIONS) {
// Remove oldest subscription
const oldest = subscriptions.values().next().value;
ws.unsubscribe('stock', [oldest]);
subscriptions.delete(oldest);
}
ws.subscribe('stock', [code]);
subscriptions.add(code);
}
Troubleshootingβ
Connection Issuesβ
Problem: Cannot connect to WebSocket
Solutions:
- Check if backend server is running:
curl http://localhost:8000/health - Verify WebSocket URL (ws:// for http, wss:// for https)
- Check JWT token validity:
curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/v1/users/me
Rate Limitingβ
Problem: Getting RATE_LIMIT_EXCEEDED errors
Solutions:
- Reduce subscription count
- Unsubscribe from unused stocks
- Use market/sector subscriptions instead of individual stocks
- Contact support to increase rate limit for your tier
High Latencyβ
Problem: Messages arriving with >100ms delay
Solutions:
- Enable compression on client
- Check network latency:
ping api.screener.kr - Verify server load:
GET /v1/ws/stats - Consider using regional endpoint
Session Restoration Failuresβ
Problem: Cannot restore session after disconnect
Solutions:
- Session may have expired (5-minute TTL)
- Verify user ID matches original connection
- Create new connection if session not found
Supportβ
For issues or questions:
- Documentation: https://docs.screener.kr
- Email: kcenon@gmail.com
- GitHub Issues: https://github.com/kcenon/screener_system/issues