Migration Guide: REST Polling to WebSocket
This guide walks you through migrating from REST API polling to real-time WebSocket streaming, helping you reduce latency, API calls, and infrastructure costs.
Why Migrate?
The Problem with Polling
REST polling creates several challenges:
sequenceDiagram
participant App as Your App
participant API as OilPriceAPI
loop Every 60 seconds
App->>API: GET /v1/prices/latest
API-->>App: Price data (maybe unchanged)
Note over App: 30-60 second latency<br/>to detect price changes
end
Note over App,API: 1,440 API calls/day<br/>~43,200 calls/month<br/>per commodity
The WebSocket Advantage
WebSocket delivers updates instantly:
sequenceDiagram
participant App as Your App
participant WS as WebSocket
App->>WS: Connect once
WS-->>App: Connection confirmed
Note over App,WS: Persistent connection
WS-->>App: Price update (instant)
WS-->>App: Price update (instant)
WS-->>App: Price update (instant)
Note over App,WS: Real-time updates<br/>No polling required
Side-by-Side Comparison
| Metric | REST Polling (1min) | REST Polling (5min) | WebSocket |
|---|---|---|---|
| Avg latency to detect change | 30 seconds | 2.5 minutes | <100ms |
| API calls per day | 1,440 | 288 | 1 |
| Monthly API calls (per commodity) | 43,200 | 8,640 | ~30 |
| Data freshness | Up to 1 min stale | Up to 5 min stale | Real-time |
| Bandwidth usage | High (repeated full responses) | Medium | Low (deltas only) |
| Server load | High | Medium | Minimal |
| Connection overhead | Per-request TLS handshake | Per-request TLS handshake | Single connection |
Cost Comparison
Assuming 5 commodities tracked:
| Approach | Monthly API Calls | Plan Required | Monthly Cost |
|---|---|---|---|
| 1-min polling | 216,000 | Business ($99) | $99/mo |
| 5-min polling | 43,200 | Starter ($25) | $25/mo |
| WebSocket | ~150 | Reservoir Mastery ($129) | $129/mo |
WebSocket value proposition:
- Reservoir Mastery includes 250,000 API calls + unlimited WebSocket
- Real-time data justifies premium for trading/monitoring apps
- Reduced infrastructure complexity saves engineering time
Migration Steps
Step 1: Audit Your Current Implementation
Before migrating, document your current polling setup:
// BEFORE: Typical polling implementation
class PricePollingService {
constructor() {
this.prices = {};
this.pollInterval = 60000; // 1 minute
}
startPolling() {
// Initial fetch
this.fetchPrices();
// Schedule polling
this.intervalId = setInterval(() => {
this.fetchPrices();
}, this.pollInterval);
}
async fetchPrices() {
const response = await fetch(
"https://api.oilpriceapi.com/v1/prices/latest",
{
headers: {
Authorization: `Token ${API_KEY}`,
},
},
);
const data = await response.json();
// Check for changes
for (const price of data.prices) {
if (this.prices[price.code] !== price.price) {
this.onPriceChange(price);
this.prices[price.code] = price.price;
}
}
}
onPriceChange(price) {
console.log(`Price changed: ${price.code} = $${price.price}`);
// Update UI, trigger alerts, etc.
}
stopPolling() {
clearInterval(this.intervalId);
}
}
Checklist:
- [ ] Document which endpoints you poll
- [ ] Note polling intervals
- [ ] Identify all places where price data is used
- [ ] List any price-change detection logic
- [ ] Document error handling and retry logic
Step 2: Install WebSocket Dependencies
::: code-group
npm install @rails/actioncable
pip install websocket-client
go get github.com/gorilla/websocket
gem install faye-websocket eventmachine
:::
Step 3: Create WebSocket Service
Build a drop-in replacement that maintains the same interface:
// AFTER: WebSocket implementation with same interface
import { createConsumer } from "@rails/actioncable";
class PriceWebSocketService {
constructor() {
this.prices = {};
this.cable = null;
this.subscription = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
}
startStreaming() {
this.cable = createConsumer("wss://api.oilpriceapi.com/cable");
this.subscription = this.cable.subscriptions.create(
{
channel: "EnergyPricesChannel",
api_key: API_KEY,
},
{
connected: () => {
console.log("WebSocket connected");
this.reconnectAttempts = 0;
},
disconnected: () => {
console.log("WebSocket disconnected");
this.handleDisconnect();
},
rejected: () => {
console.error("Subscription rejected - check API key");
},
received: (message) => {
this.handleMessage(message);
},
},
);
}
handleMessage(message) {
if (message.type === "price_update") {
const price = message.data;
// Same change detection as polling version
if (this.prices[price.code] !== price.price) {
this.onPriceChange(price);
this.prices[price.code] = price.price;
}
}
}
// Same interface as polling version!
onPriceChange(price) {
console.log(`Price changed: ${price.code} = $${price.price}`);
// Update UI, trigger alerts, etc.
}
handleDisconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(() => this.startStreaming(), delay);
}
}
stopStreaming() {
if (this.subscription) {
this.subscription.unsubscribe();
}
if (this.cable) {
this.cable.disconnect();
}
}
}
Step 4: Implement Feature Flag
Use a feature flag for gradual rollout:
class PriceService {
constructor() {
this.useWebSocket = process.env.USE_WEBSOCKET === "true";
if (this.useWebSocket) {
this.provider = new PriceWebSocketService();
} else {
this.provider = new PricePollingService();
}
}
start() {
if (this.useWebSocket) {
this.provider.startStreaming();
} else {
this.provider.startPolling();
}
}
stop() {
if (this.useWebSocket) {
this.provider.stopStreaming();
} else {
this.provider.stopPolling();
}
}
// Delegate to current provider
onPriceChange(callback) {
this.provider.onPriceChange = callback;
}
}
Step 5: Add Fallback Logic
Implement automatic fallback to polling if WebSocket fails:
class ResilientPriceService {
constructor() {
this.webSocket = new PriceWebSocketService();
this.polling = new PricePollingService();
this.activeProvider = "websocket";
this.webSocketFailures = 0;
this.maxWebSocketFailures = 3;
}
start() {
// Start with WebSocket
this.webSocket.startStreaming();
// Monitor WebSocket health
this.webSocket.onDisconnect = () => {
this.webSocketFailures++;
if (this.webSocketFailures >= this.maxWebSocketFailures) {
console.warn("WebSocket unreliable, falling back to polling");
this.fallbackToPolling();
}
};
this.webSocket.onConnect = () => {
// WebSocket recovered, switch back
if (this.activeProvider === "polling") {
console.log("WebSocket recovered, switching back");
this.polling.stopPolling();
this.activeProvider = "websocket";
}
this.webSocketFailures = 0;
};
}
fallbackToPolling() {
this.activeProvider = "polling";
this.polling.startPolling();
}
stop() {
this.webSocket.stopStreaming();
this.polling.stopPolling();
}
}
Step 6: Update UI Components
Ensure UI handles real-time updates efficiently:
// BEFORE: UI updated on poll
function updatePriceDisplay(prices) {
for (const price of prices) {
document.getElementById(`price-${price.code}`).textContent =
`$${price.price}`;
}
}
// AFTER: UI updated on each message with animation
function updatePriceDisplay(price) {
const element = document.getElementById(`price-${price.code}`);
// Store previous value for comparison
const previous = parseFloat(element.dataset.price || 0);
const current = price.price;
// Update display
element.textContent = `$${current.toFixed(2)}`;
element.dataset.price = current;
// Add visual feedback for changes
if (current > previous) {
element.classList.add("price-up");
setTimeout(() => element.classList.remove("price-up"), 1000);
} else if (current < previous) {
element.classList.add("price-down");
setTimeout(() => element.classList.remove("price-down"), 1000);
}
}
/* Add price change animations */
.price-up {
animation: flash-green 1s ease-out;
}
.price-down {
animation: flash-red 1s ease-out;
}
@keyframes flash-green {
0% {
background-color: #00ff0033;
}
100% {
background-color: transparent;
}
}
@keyframes flash-red {
0% {
background-color: #ff000033;
}
100% {
background-color: transparent;
}
}
Step 7: Update Tests
Update tests to work with both providers:
describe("PriceService", () => {
describe("with WebSocket", () => {
let service;
let mockCable;
beforeEach(() => {
process.env.USE_WEBSOCKET = "true";
mockCable = createMockCable();
service = new PriceService();
});
it("receives real-time updates", async () => {
const priceUpdates = [];
service.onPriceChange((price) => priceUpdates.push(price));
service.start();
// Simulate WebSocket message
mockCable.simulateMessage({
type: "price_update",
data: { code: "WTI_USD", price: 75.5 },
});
expect(priceUpdates).toHaveLength(1);
expect(priceUpdates[0].price).toBe(75.5);
});
it("reconnects on disconnect", async () => {
service.start();
mockCable.simulateDisconnect();
await waitFor(1500); // Wait for reconnect delay
expect(mockCable.connectionAttempts).toBe(2);
});
});
describe("with REST polling (fallback)", () => {
beforeEach(() => {
process.env.USE_WEBSOCKET = "false";
});
// ... existing polling tests
});
});
Step 8: Gradual Rollout
- Development: Enable WebSocket for all developers
- Staging: Full WebSocket testing
- Production 10%: Enable for 10% of users via feature flag
- Production 50%: Increase to 50% if metrics look good
- Production 100%: Full rollout
- Cleanup: Remove polling code after 2-week observation period
Rollback Strategy
If issues arise, rollback should be instant:
// Environment variable toggle
// .env: USE_WEBSOCKET=false
// Or runtime toggle via API
async function toggleWebSocket(enabled) {
await fetch("/api/config", {
method: "POST",
body: JSON.stringify({ websocket_enabled: enabled }),
});
// Service picks up new config on next heartbeat
}
Rollback triggers:
- WebSocket connection success rate < 95%
- Latency p95 > 5 seconds
- Memory usage increasing over time (connection leak)
- User reports of stale data
Common Pitfalls
1. Not Handling Reconnection
Problem: Connection drops and app never reconnects.
// BAD: No reconnection logic
this.ws = new WebSocket(url);
this.ws.onclose = () => console.log("Disconnected");
// GOOD: Exponential backoff reconnection
this.ws.onclose = () => {
const delay = Math.min(1000 * Math.pow(2, this.attempts), 30000);
setTimeout(() => this.connect(), delay);
this.attempts++;
};
2. Memory Leaks from Event Listeners
Problem: Creating new listeners on each reconnect.
// BAD: Listeners accumulate
connect() {
this.ws = new WebSocket(url);
this.ws.onmessage = (e) => this.handleMessage(e);
}
// GOOD: Clean up before reconnect
connect() {
if (this.ws) {
this.ws.onmessage = null;
this.ws.close();
}
this.ws = new WebSocket(url);
this.ws.onmessage = (e) => this.handleMessage(e);
}
3. Blocking on Message Processing
Problem: Slow message processing blocks subsequent messages.
// BAD: Synchronous heavy processing
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
updateDatabase(data); // Blocking!
recalculatePortfolio(data); // Blocking!
renderCharts(data); // Blocking!
};
// GOOD: Queue and process asynchronously
const messageQueue = [];
ws.onmessage = (e) => {
messageQueue.push(JSON.parse(e.data));
};
// Process queue in batches
setInterval(() => {
const batch = messageQueue.splice(0, 100);
if (batch.length > 0) {
processBatch(batch);
}
}, 100);
4. Ignoring Connection State
Problem: Sending messages while disconnected.
// BAD: No state check
function subscribe(commodities) {
ws.send(JSON.stringify({ subscribe: commodities }));
}
// GOOD: Check state first
function subscribe(commodities) {
if (ws.readyState !== WebSocket.OPEN) {
console.error("Cannot subscribe: not connected");
this.pendingSubscriptions.push(commodities);
return;
}
ws.send(JSON.stringify({ subscribe: commodities }));
}
5. Not Testing Failure Scenarios
Problem: App works in happy path but fails in production.
// Test these scenarios:
describe("WebSocket edge cases", () => {
it("handles server-initiated disconnect");
it("handles network timeout");
it("handles invalid JSON messages");
it("handles rapid connect/disconnect cycles");
it("handles message during reconnect");
it("handles subscription rejection");
});
Migration Checklist
- [ ] Audit current polling implementation
- [ ] Verify Reservoir Mastery subscription active
- [ ] Install WebSocket dependencies
- [ ] Implement WebSocket service
- [ ] Add reconnection with exponential backoff
- [ ] Implement feature flag
- [ ] Add fallback to polling
- [ ] Update UI for real-time updates
- [ ] Add connection status indicator
- [ ] Update tests for both providers
- [ ] Test in staging environment
- [ ] Plan gradual rollout
- [ ] Document rollback procedure
- [ ] Monitor metrics post-rollout
Performance Benchmarks
After migration, you should see:
| Metric | Before (Polling) | After (WebSocket) | Improvement |
|---|---|---|---|
| Time to detect price change | 30-60s | <1s | 60x faster |
| API calls per day | 1,440+ | 1 | 99.9% reduction |
| Bandwidth per commodity | ~500KB/day | ~50KB/day | 90% reduction |
| Server connections | 1 per request | 1 persistent | Stable |
Related Documentation
- WebSocket API Reference - Implementation examples
- Architecture - Technical deep-dive
- Troubleshooting - Common issues