Oil Price API Documentation - Quick Start in 5 Minutes | REST API
GitHub
GitHub

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

MetricREST Polling (1min)REST Polling (5min)WebSocket
Avg latency to detect change30 seconds2.5 minutes<100ms
API calls per day1,4402881
Monthly API calls (per commodity)43,2008,640~30
Data freshnessUp to 1 min staleUp to 5 min staleReal-time
Bandwidth usageHigh (repeated full responses)MediumLow (deltas only)
Server loadHighMediumMinimal
Connection overheadPer-request TLS handshakePer-request TLS handshakeSingle connection

Cost Comparison

Assuming 5 commodities tracked:

ApproachMonthly API CallsPlan RequiredMonthly Cost
1-min polling216,000Business ($99)$99/mo
5-min polling43,200Starter ($25)$25/mo
WebSocket~150Reservoir 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

  1. Development: Enable WebSocket for all developers
  2. Staging: Full WebSocket testing
  3. Production 10%: Enable for 10% of users via feature flag
  4. Production 50%: Increase to 50% if metrics look good
  5. Production 100%: Full rollout
  6. 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:

MetricBefore (Polling)After (WebSocket)Improvement
Time to detect price change30-60s<1s60x faster
API calls per day1,440+199.9% reduction
Bandwidth per commodity~500KB/day~50KB/day90% reduction
Server connections1 per request1 persistentStable

Related Documentation

  • WebSocket API Reference - Implementation examples
  • Architecture - Technical deep-dive
  • Troubleshooting - Common issues
Last Updated: 2/3/26, 1:27 AM