How do you get a live gold price into an app without polling a REST endpoint in a loop Most commodities price APIs are REST-only, so the standard workaround is a timer that fetches a spot quote every few seconds. That timer burns through a monthly call quota, adds a request's worth of latency to every reading, and still leaves the displayed price as old as the polling interval. A WebSocket commodities feed removes the loop entirely: open one connection, subscribe to XAUUSD, XAGUSD, and WTIUSD, and the server pushes each new price as it's published.
This post covers streaming precious metals and energy prices from SiftingIO's WebSocket API: the subscribe protocol, the exact shape of a tick, and a reconnect-safe client you can drop into a dashboard or an alerting job. For one-off fetches of the same symbols over REST, see getting gold and oil spot prices via API. This post is about keeping a price continuously current.
Why polling a commodities price API doesn't scale#
Suppose a dashboard shows gold, silver, and crude and refreshes once a second. That's three REST calls per second, about 259,000 calls per day, per browser tab if the polling runs client-side. Metered plans price REST by monthly call volume, so a polling loop converts directly into quota: SiftingIO's free tier includes 10,000 REST calls a month, which one-second polling of a single symbol exhausts in under three hours.
Slower polling saves quota but caps freshness. Poll every 30 seconds and every reading is up to 30 seconds stale, which is a long time for an alert that fires when crude crosses a threshold.
A push feed changes the arithmetic. The free tier includes one WebSocket connection with five concurrent symbol subscriptions, and updates arrive as the server publishes them with no per-message metering. The connection is the cost, and there's exactly one.
One connection, gold, silver, and oil#
Authentication happens in the URL with the ?key= query parameter (or an explicit {"op":"auth","key":"..."} frame after connecting). After auth succeeds the server replies with an ack frame that includes your plan tier. Commodities live under the com product; symbols are the USD-pair codes from the symbol catalog: XAUUSD for gold, XAGUSD for silver, WTIUSD and BRENTUSD for crude, NATGASUSD for natural gas.
import WebSocket from "ws"; // Node 18+; browsers have WebSocket built in
const ws = new WebSocket(
`wss://stream.sifting.io/ws/v1?key=${process.env.SIFTING_KEY}`
);
ws.on("open", () => {
ws.send(JSON.stringify({
op: "subscribe",
product: "com",
symbols: ["XAUUSD", "XAGUSD", "WTIUSD"]
}));
});
ws.on("message", (raw) => {
const msg = JSON.parse(raw);
if (msg.f === "tick") {
console.log(msg.s, msg.p, "bid", msg.b, "ask", msg.a,
new Date(msg.t).toISOString());
}
});
Server frames discriminate on the f field. A price update looks like:
{ "f": "tick", "s": "XAUUSD", "p": 0.0, "b": 0.0, "B": 0.0, "a": 0.0, "A": 0.0, "t": 1778019852426 }
p is the last price, b and a are bid and ask, B and A their sizes, and t is a Unix epoch timestamp in milliseconds. Immediately after a subscribe the server sends the last cached value for each symbol, then live updates. That first frame lets you paint the UI without a blank state, but treat its timestamp with suspicion (more on that below).
A reconnect-safe client#
Long-lived connections drop. Wi-Fi changes, laptops sleep, load balancers rotate. A production client needs three behaviors: a keepalive ping, resubscription on reconnect, and stale-tick rejection so the cached value replayed after a reconnect can't overwrite a newer price.
import WebSocket from "ws";
const SYMBOLS = ["XAUUSD", "XAGUSD", "WTIUSD", "BRENTUSD", "NATGASUSD"];
const last = new Map();
function handleTick(tick) {
const prev = last.get(tick.s);
if (prev & tick.t <= prev.t) return; // replayed cache or out-of-order frame
last.set(tick.s, tick);
// update the UI, evaluate alert rules, append to your store
}
function connect(attempt = 0) {
const ws = new WebSocket(
`wss://stream.sifting.io/ws/v1?key=${process.env.SIFTING_KEY}`
);
let pingTimer;
ws.on("open", () => {
attempt = 0;
ws.send(JSON.stringify({ op: "subscribe", product: "com", symbols: SYMBOLS }));
pingTimer = setInterval(
() => ws.send(JSON.stringify({ op: "ping" })),
30_000
);
});
ws.on("message", (raw) => {
const msg = JSON.parse(raw);
if (msg.f === "tick") handleTick(msg);
if (msg.f === "error") console.error("stream error:", msg.code);
});
ws.on("close", () => {
clearInterval(pingTimer);
const delay = Math.min(30_000, 1_000 * 2 ** attempt);
setTimeout(() => connect(attempt + 1), delay);
});
ws.on("error", () => ws.close());
}
connect();
Five symbols is a deliberate choice: it fits exactly inside the free tier's five-subscription limit, so this snippet runs on a key from a free account. A sixth symbol gets an error frame with code max_subscriptions and the subscribe is rejected.
What the price you're streaming actually is#
Each tick carries a consensus price aggregated from multiple independent venues, formed as a volume- and reputation-weighted median with staleness and outlier filtering in front of it. One venue printing a bad gold price doesn't move the published value; a majority of venues would have to err in the same direction. The method is documented at /data-methodology.
Update cadence depends on plan tier: the free tier publishes the fair price at 1 Hz, Builder at 4 Hz, Pro at 6 Hz, and Ultra at 10 Hz. Those are fair-price update rates for the consensus value. The stream is a reference feed for display, alerting, and analytics; it isn't an execution feed and shouldn't be treated as executable depth.
Common pitfalls#
The idle timeout only counts client frames. The server closes any connection that's been silent for 90 seconds, and inbound ticks don't reset that timer. Only client-to-server frames do. A client that subscribes and then just listens gets disconnected mid-stream even while receiving data, which looks like a mysterious drop every minute and a half. Send {"op":"ping"} at least every 60 seconds; the client above uses 30.
The first frame after subscribe can be hours old. Metals and energy trade roughly around the clock on weekdays but pause over the weekend. Subscribe on a Saturday and the cached XAUUSD value you receive dates from the Friday close. An alerting job that treats that replayed frame as a fresh print can fire on stale data, so always check t before acting on a tick and gate alerts on recency.
Reconnect loops can strand your only connection slot. The free tier allows one concurrent connection, and an implementation that opens a new socket before the old one has fully closed can hit an error frame with code max_connections. A crash-looping process can lock itself out until the server reaps the dead connection. Reconnect from the close event, never from error alone, and back off exponentially as the client above does.
One smaller trap: the product name is com, and sending "product": "commodities" returns an unknown_product error frame.
Streaming needs a key but no card. Start building free: the free tier includes one WebSocket connection and five symbol subscriptions.



