OilPriceAPI Docs
GitHub
GitHub

Migration Guide: REST Polling to WebSocket

This guide walks you through migrating from REST polling to source-aware WebSocket delivery. The goal is to avoid unnecessary polling and detect an OilPriceAPI update without waiting for the next poll. It does not make an upstream source publish more frequently.

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 pushes a payload after OilPriceAPI receives an upstream update:

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 after source publication
    WS-->>App: Price update after source publication
    WS-->>App: Keep-alive ping during a quiet period

    Note over App,WS: Push delivery<br/>REST backfill still required after disconnects

Side-by-Side Comparison

MetricREST pollingWebSocket
Source freshnessDetermined by the upstream sourceDetermined by the same upstream source
Detection delayBounded by your poll interval after OilPriceAPI receives an updatePush after OilPriceAPI receives an update; measure at your client
Quota usageOne request per poll, including unchanged responsesPersistent connection plus reconnects
Gap recoveryRequest the next snapshot/history windowReconnect, then request history since the last stored source timestamp

Quota comparison

Calculate polling usage as polls per day × days × requests per poll, then compare it with the current limits on the pricing page. Do not poll more frequently than your workflow requires: repeated requests cannot make an unchanged upstream source newer. WebSocket access is currently a Professional-or-higher capability.

WebSocket value proposition:

  • Detect OilPriceAPI updates without waiting for the next scheduled poll
  • Avoid quota usage on repeatedly unchanged snapshots
  • Keep REST history as a recovery path rather than treating the stream as exactly-once

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?token=${encodeURIComponent(API_KEY)}`,
    );

    this.subscription = this.cable.subscriptions.create(
      {
        channel: "EnergyPricesChannel",
      },
      {
        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 Professional (or higher) 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

Record a baseline before rollout and compare these measurements in your own environment:

MeasurementBefore (polling)After (WebSocket)
Source timestamp → client receiptMeasure from stored responsesMeasure from delivered payloads
Requests/connections per dayCount successful and failed pollsCount connects, reconnects, and REST backfills
Missed-update recoveryDocument current behaviorVerify history backfill after a forced disconnect
Customer-visible errorsRecord timeout/quota/auth statesRecord rejected/disconnected/backfill states

Related Documentation

  • WebSocket API Reference - Implementation examples
  • Architecture - Technical deep-dive
  • Troubleshooting - Common issues
Last Updated: 7/21/26, 9:17 PM