WebSocket Architecture
This document provides a technical deep-dive into OilPriceAPI's WebSocket infrastructure, designed for developers who need to understand the system internals for optimal integration.
System Overview
OilPriceAPI uses Ruby on Rails ActionCable for WebSocket communication, backed by Redis PubSub for message distribution across multiple server instances.
flowchart LR
subgraph Clients
C1[Web App]
C2[Mobile App]
C3[Trading System]
end
subgraph OilPriceAPI Infrastructure
LB[Load Balancer]
subgraph Rails Servers
R1[Rails + ActionCable]
R2[Rails + ActionCable]
R3[Rails + ActionCable]
end
REDIS[(Redis PubSub)]
subgraph Data Pipeline
PS[Price Scrapers]
DS[Drilling Sources]
CACHE[(Price Cache)]
end
end
subgraph External Sources
EX1[ICE Exchange]
EX2[CME Group]
EX3[EIA Data]
EX4[Baker Hughes]
end
C1 & C2 & C3 --> LB
LB --> R1 & R2 & R3
R1 & R2 & R3 <--> REDIS
PS & DS --> CACHE
CACHE --> REDIS
EX1 & EX2 & EX3 & EX4 --> PS & DS
Connection Lifecycle
Every WebSocket connection follows this precise sequence:
sequenceDiagram
participant Client
participant LoadBalancer
participant ActionCable
participant Redis
participant AuthService
Client->>LoadBalancer: WSS Handshake Request
LoadBalancer->>ActionCable: Forward Connection
ActionCable->>Client: WebSocket Established
Note over Client,ActionCable: Connection Open
Client->>ActionCable: Subscribe Command<br/>{channel, api_key}
ActionCable->>AuthService: Validate API Key
AuthService-->>ActionCable: User + Plan Details
alt Invalid Key or Plan
ActionCable->>Client: Subscription Rejected
else Valid Reservoir Mastery
ActionCable->>Redis: SUBSCRIBE energy_prices_{user_id}
ActionCable->>Client: Subscription Confirmed
ActionCable->>Client: Welcome Message + Initial Data
end
loop Price Updates
Redis->>ActionCable: New Price Data
ActionCable->>Client: price_update Message
end
loop Heartbeat (every 3s)
ActionCable->>Client: ping
Client->>ActionCable: pong
end
Note over Client,ActionCable: Connection Maintained
Client->>ActionCable: Unsubscribe / Close
ActionCable->>Redis: UNSUBSCRIBE
ActionCable->>Client: Connection Closed
Connection States
| State | Description | Client Action |
|---|---|---|
connecting | WebSocket handshake in progress | Wait for open event |
open | Connection established | Send subscribe command |
subscribed | Channel subscription active | Process incoming messages |
disconnected | Connection lost | Trigger reconnection logic |
rejected | Subscription denied | Check API key and plan |
Channel Architecture
EnergyPricesChannel
The primary channel for all real-time price data.
# Server-side channel implementation (simplified)
class EnergyPricesChannel < ApplicationCable::Channel
def subscribed
# Validate Reservoir Mastery subscription
reject unless current_user&.reservoir_mastery?
# Subscribe to user-specific stream
stream_from "energy_prices_#{current_user.id}"
# Also subscribe to public broadcast stream
stream_from "energy_prices_public"
# Send initial price data
transmit(type: 'welcome', data: current_prices)
end
def unsubscribed
# Cleanup on disconnect
stop_all_streams
end
end
Stream Types
| Stream | Purpose | Content |
|---|---|---|
energy_prices_{user_id} | Private user stream | User-specific alerts, filtered updates |
energy_prices_public | Shared broadcast | All commodity price updates |
Message Types
flowchart TD
subgraph Incoming Messages
M1[welcome]
M2[price_update]
M3[drilling_intelligence_update]
M4[alert]
M5[ping]
end
M1 --> |Initial connection| D1[Initial price snapshot]
M2 --> |Price change| D2[Commodity price data]
M3 --> |Weekly/Daily| D3[Rig counts, permits, etc.]
M4 --> |Threshold breach| D4[Custom alert data]
M5 --> |Heartbeat| D5[Connection keepalive]
Data Flow Pipeline
Price Update Flow
flowchart LR
subgraph Data Sources
ICE[ICE Exchange]
CME[CME Group]
TR[Trading Economics]
end
subgraph Scraper Workers
SW1[PriceScraper]
SW2[FuturesScraper]
end
subgraph Processing
VAL[Validation]
CACHE[(Redis Cache)]
DB[(PostgreSQL)]
end
subgraph Broadcast
WS[WebSocket Service]
REDIS[(Redis PubSub)]
end
subgraph Clients
C1[Client 1]
C2[Client 2]
C3[Client N]
end
ICE & CME & TR --> SW1 & SW2
SW1 & SW2 --> VAL
VAL --> CACHE
VAL --> DB
CACHE --> WS
WS --> REDIS
REDIS --> C1 & C2 & C3
Update Frequency
| Data Type | Source | Update Frequency | Cache TTL |
|---|---|---|---|
| Spot Prices | ICE, CME | Every 30 seconds during market hours | 30 seconds |
| Futures | ICE, CME | Every 60 seconds | 60 seconds |
| Rig Counts | Baker Hughes | Weekly (Friday 1PM ET) | 1 hour |
| Well Permits | State agencies | Daily (varies by state) | 6 hours |
| Frac Spreads | Primary Vision | Daily | 6 hours |
Authentication Flow
sequenceDiagram
participant Client
participant ActionCable
participant TokenAuth
participant UserModel
participant SubscriptionService
Client->>ActionCable: Connect with api_key
ActionCable->>TokenAuth: find_by_token(api_key)
TokenAuth->>UserModel: Lookup user
alt Token Invalid
UserModel-->>ActionCable: nil
ActionCable->>Client: Connection Rejected
else Token Valid
UserModel-->>TokenAuth: User record
TokenAuth->>SubscriptionService: Check plan
SubscriptionService-->>TokenAuth: Plan details
alt Not Reservoir Mastery
TokenAuth-->>ActionCable: Insufficient plan
ActionCable->>Client: Subscription Rejected
else Reservoir Mastery Active
TokenAuth-->>ActionCable: Authorized
ActionCable->>Client: Subscription Confirmed
end
end
Performance Characteristics
Latency Benchmarks
| Metric | Value | Notes |
|---|---|---|
| Connection establishment | 50-150ms | Includes TLS handshake |
| Subscription confirmation | 20-50ms | After connection open |
| Price update delivery | 30-80ms | From source to client |
| End-to-end latency | 100-300ms | Source change to client receipt |
Throughput Limits
| Resource | Limit | Per |
|---|---|---|
| Concurrent connections | 10 | API key |
| Messages received | Unlimited | - |
| Messages sent (client to server) | 10/second | Connection |
| Subscriptions | 1 | Connection |
Resource Usage
pie title Server Resource Allocation per Connection
"Memory (per connection)" : 2
"CPU (idle)" : 1
"CPU (active message)" : 5
"Redis subscription" : 1
Per-connection overhead:
- Memory: ~2KB idle, ~10KB during message processing
- Redis: 1 SUBSCRIBE per user stream
- File descriptors: 1 per connection
High Availability Design
Multi-Server Architecture
flowchart TB
subgraph Region 1
LB1[Load Balancer]
R1A[Rails Server A]
R1B[Rails Server B]
RED1[(Redis Primary)]
end
subgraph Region 2
LB2[Load Balancer]
R2A[Rails Server A]
R2B[Rails Server B]
RED2[(Redis Replica)]
end
GLB[Global Load Balancer]
GLB --> LB1 & LB2
LB1 --> R1A & R1B
LB2 --> R2A & R2B
R1A & R1B <--> RED1
R2A & R2B <--> RED2
RED1 <-.-> |Replication| RED2
Failover Behavior
- Server failure: Load balancer routes to healthy servers; clients reconnect automatically
- Redis failure: Failover to replica within 30 seconds; brief message gap possible
- Region failure: DNS failover to secondary region within 60 seconds
Connection Cleanup
Stale connections are cleaned every 15 minutes:
- Connections with no ping response in 60 seconds
- Connections from revoked API keys
- Connections exceeding per-user limits
Security Model
Transport Security
| Layer | Protection |
|---|---|
| Transport | TLS 1.3 (minimum 1.2) |
| Origin validation | Checked against allowed origins |
| Rate limiting | Connection attempts limited per IP |
Authentication Security
flowchart TD
REQ[Connection Request]
REQ --> IP[IP Rate Check]
IP -->|Exceeded| BLOCK[Block 429]
IP -->|OK| TOKEN[Token Validation]
TOKEN -->|Invalid| REJECT1[Reject 401]
TOKEN -->|Valid| PLAN[Plan Check]
PLAN -->|Not RM| REJECT2[Reject 403]
PLAN -->|RM Active| CONN[Connection Check]
CONN -->|Limit Exceeded| REJECT3[Reject 429]
CONN -->|Under Limit| ACCEPT[Accept Connection]
Data Protection
- No sensitive data transmitted over WebSocket
- API keys used only for authentication, not in message payloads
- Price data is non-PII and suitable for broadcast
Monitoring & Debugging
Key Metrics
Monitor these metrics for optimal WebSocket performance:
| Metric | Healthy Range | Alert Threshold |
|---|---|---|
| Active connections | 0-10,000 | >8,000 |
| Message latency (p95) | <100ms | >500ms |
| Connection errors/min | <10 | >50 |
| Redis pub/sub lag | <10ms | >100ms |
Debug Logging
Enable debug logging in your client to troubleshoot issues:
// ActionCable debug mode
ActionCable.logger.enabled = true;
// Native WebSocket verbose logging
const ws = new WebSocket("wss://api.oilpriceapi.com/cable");
ws.onmessage = (e) => console.log("WS Message:", e.data);
ws.onerror = (e) => console.error("WS Error:", e);
Common Issues
| Symptom | Likely Cause | Solution |
|---|---|---|
| Connection drops every 60s | Missing ping/pong | Implement heartbeat handling |
| No messages after subscribe | Wrong channel name | Verify EnergyPricesChannel |
| Subscription rejected | Invalid/expired API key | Check key and plan status |
| Intermittent disconnects | Network instability | Implement exponential backoff reconnection |
Related Documentation
- WebSocket API Reference - Implementation examples and message formats
- Migration Guide - Moving from REST polling to WebSocket
- Troubleshooting Guide - Common issues and solutions